mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 10:37:13 -04:00 
			
		
		
		
	Rework images & skeletons
This commit is contained in:
		
							parent
							
								
									886b33d5a7
								
							
						
					
					
						commit
						e63e3605c6
					
				| @ -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<Serie["watchStatus"]>["status"]; | ||||
| 
 | ||||
| export const EpisodesContext = ({ | ||||
| 	type = "episode", | ||||
| 	slug, | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import z from "zod"; | ||||
| import { z } from "zod/v4"; | ||||
| 
 | ||||
| export const Entry = z.object({ | ||||
| 	id: z.string(), | ||||
|  | ||||
| @ -8,3 +8,5 @@ export * from "./show"; | ||||
| export * from "./entry"; | ||||
| export * from "./studio"; | ||||
| export * from "./video"; | ||||
| 
 | ||||
| export * from "./utils/images"; | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -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<typeof Show>; | ||||
| 
 | ||||
| export type WatchStatusV = NonNullable<Serie["watchStatus"]>["status"]; | ||||
| export const WatchStatusV = ["completed", "watching", "rewatching", "dropped", "planned"] as const; | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import z from "zod"; | ||||
| import z from "zod/v4"; | ||||
| 
 | ||||
| export const Genre = z.enum([ | ||||
| 	"action", | ||||
|  | ||||
| @ -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<typeof Image>; | ||||
| export type KImage = z.infer<typeof KImage>; | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -1,20 +1,16 @@ | ||||
| import { z } from "zod"; | ||||
| import { type ZodType, z } from "zod/v4"; | ||||
| 
 | ||||
| export interface Page<T> { | ||||
| export type Page<T> = { | ||||
| 	this: string; | ||||
| 	first: string; | ||||
| 	next: string | null; | ||||
| 	count: number; | ||||
| 	items: T[]; | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| export const Paged = <Item>(item: z.ZodType<Item>): z.ZodSchema<Page<Item>> => | ||||
| export const Paged = <Parser extends ZodType>(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 = <T = unknown>(obj: unknown): obj is Page<T> => | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| import { z } from "zod"; | ||||
| import { z } from "zod/v4"; | ||||
| 
 | ||||
| export const zdate = z.coerce.date; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { z } from "zod"; | ||||
| import { z } from "zod/v4"; | ||||
| 
 | ||||
| export const EmbeddedVideo = z.object({ | ||||
| 	id: z.string(), | ||||
|  | ||||
| @ -1 +0,0 @@ | ||||
| export const imageBorderRadius = 10; | ||||
							
								
								
									
										95
									
								
								front/src/primitives/image-background.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								front/src/primitives/image-background.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 ( | ||||
| 		<EImageBackground | ||||
| 			source={{ | ||||
| 				uri: src ? `${apiUrl}${src[quality ?? "high"]}` : null, | ||||
| 				headers: authToken | ||||
| 					? { | ||||
| 							Authorization: authToken, | ||||
| 						} | ||||
| 					: {}, | ||||
| 			}} | ||||
| 			placeholder={{ blurhash: src?.blurhash }} | ||||
| 			accessibilityLabel={alt} | ||||
| 			{...(css(layout, props) as any)} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| export const PosterBackground = ({ | ||||
| 	alt, | ||||
| 	layout, | ||||
| 	...props | ||||
| }: Omit<ComponentProps<typeof ImageBackground>, "layout"> & { | ||||
| 	style?: ImageStyle; | ||||
| 	layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; | ||||
| }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<ImageBackground | ||||
| 			alt={alt!} | ||||
| 			layout={{ aspectRatio: 2 / 3, ...layout }} | ||||
| 			{...css({ borderRadius: 6 }, props)} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| export const GradientImageBackground = ({ | ||||
| 	gradient, | ||||
| 	children, | ||||
| 	...props | ||||
| }: ComponentProps<typeof ImageBackground> & { | ||||
| 	gradient?: Partial<LinearGradientProps>; | ||||
| }) => { | ||||
| 	const { css, theme } = useYoshiki(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<ImageBackground {...props}> | ||||
| 			<LinearGradient | ||||
| 				start={{ x: 0, y: 0.25 }} | ||||
| 				end={{ x: 0, y: 1 }} | ||||
| 				colors={["transparent", theme.darkOverlay]} | ||||
| 				{...css( | ||||
| 					{ | ||||
| 						position: "absolute", | ||||
| 						top: 0, | ||||
| 						bottom: 0, | ||||
| 						left: 0, | ||||
| 						right: 0, | ||||
| 					}, | ||||
| 					typeof gradient === "object" ? gradient : undefined, | ||||
| 				)} | ||||
| 			> | ||||
| 				{children} | ||||
| 			</LinearGradient> | ||||
| 		</ImageBackground> | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										76
									
								
								front/src/primitives/image.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								front/src/primitives/image.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<Style> = Style extends any | ||||
| 	? { | ||||
| 			[key in keyof Style]: YoshikiStyle<Style[key]>; | ||||
| 		} | ||||
| 	: never; | ||||
| 
 | ||||
| export type ImageLayout = YoshikiEnhanced< | ||||
| 	| { width: ImageStyle["width"]; height: ImageStyle["height"] } | ||||
| 	| { width: ImageStyle["width"]; aspectRatio: ImageStyle["aspectRatio"] } | ||||
| 	| { height: ImageStyle["height"]; aspectRatio: ImageStyle["aspectRatio"] } | ||||
| >; | ||||
| 
 | ||||
| // This should stay in think with `ImageBackground`.
 | ||||
| // (copy-pasted but change `EImageBackground` with `EImage`)
 | ||||
| export const Image = ({ | ||||
| 	src, | ||||
| 	quality, | ||||
| 	alt, | ||||
| 	layout, | ||||
| 	...props | ||||
| }: { | ||||
| 	src: KImage | null; | ||||
| 	quality: "low" | "medium" | "high"; | ||||
| 	alt?: string; | ||||
| 	style?: ImageStyle; | ||||
| 	layout: ImageLayout; | ||||
| }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 	const { apiUrl, authToken } = useToken(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<EImage | ||||
| 			source={{ | ||||
| 				uri: src ? `${apiUrl}${src[quality ?? "high"]}` : null, | ||||
| 				headers: authToken | ||||
| 					? { | ||||
| 							Authorization: authToken, | ||||
| 						} | ||||
| 					: {}, | ||||
| 			}} | ||||
| 			placeholder={{ blurhash: src?.blurhash }} | ||||
| 			accessibilityLabel={alt} | ||||
| 			{...(css([layout, { borderRadius: 6 }], props) as any)} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| Image.Loader = ({ layout, ...props }: { layout: ImageLayout }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 	const border = { borderRadius: 6 } satisfies ViewStyle; | ||||
| 
 | ||||
| 	return <Skeleton variant="custom" {...css([layout, border], props)} />; | ||||
| }; | ||||
| 
 | ||||
| export const Poster = ({ | ||||
| 	layout, | ||||
| 	...props | ||||
| }: ComponentProps<typeof Image> & { | ||||
| 	layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; | ||||
| }) => <Image layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />; | ||||
| 
 | ||||
| Poster.Loader = ({ | ||||
| 	layout, | ||||
| 	...props | ||||
| }: { | ||||
| 	layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; | ||||
| }) => <Image.Loader layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />; | ||||
| @ -1,44 +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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import type { ReactElement } from "react"; | ||||
| import type { ImageStyle } from "react-native"; | ||||
| import type { YoshikiStyle } from "yoshiki/src/type"; | ||||
| import type { KyooImage } from "~/models"; | ||||
| 
 | ||||
| export type YoshikiEnhanced<Style> = Style extends any | ||||
| 	? { | ||||
| 			[key in keyof Style]: YoshikiStyle<Style[key]>; | ||||
| 		} | ||||
| 	: never; | ||||
| 
 | ||||
| export type Props = { | ||||
| 	src?: KyooImage | null; | ||||
| 	quality: "low" | "medium" | "high"; | ||||
| 	alt?: string; | ||||
| 	Err?: ReactElement | null; | ||||
| 	forcedLoading?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export type ImageLayout = YoshikiEnhanced< | ||||
| 	| { width: ImageStyle["width"]; height: ImageStyle["height"] } | ||||
| 	| { width: ImageStyle["width"]; aspectRatio: ImageStyle["aspectRatio"] } | ||||
| 	| { height: ImageStyle["height"]; aspectRatio: ImageStyle["aspectRatio"] } | ||||
| >; | ||||
| @ -1,43 +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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import type { ReactElement } from "react"; | ||||
| import { View } from "react-native"; | ||||
| import { Blurhash } from "react-native-blurhash"; | ||||
| import { type Stylable, useYoshiki } from "yoshiki/native"; | ||||
| 
 | ||||
| export const BlurhashContainer = ({ | ||||
| 	blurhash, | ||||
| 	children, | ||||
| 	...props | ||||
| }: { blurhash: string; children?: ReactElement | ReactElement[] } & Stylable) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<View {...props}> | ||||
| 			<Blurhash | ||||
| 				blurhash={blurhash} | ||||
| 				resizeMode="cover" | ||||
| 				{...css({ position: "absolute", top: 0, bottom: 0, left: 0, right: 0 })} | ||||
| 			/> | ||||
| 			{children} | ||||
| 		</View> | ||||
| 	); | ||||
| }; | ||||
| @ -1,322 +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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import { decode } from "blurhash"; | ||||
| import { | ||||
| 	type HTMLAttributes, | ||||
| 	type ReactElement, | ||||
| 	forwardRef, | ||||
| 	useImperativeHandle, | ||||
| 	useLayoutEffect, | ||||
| 	useRef, | ||||
| 	useState, | ||||
| } from "react"; | ||||
| import { useYoshiki } from "yoshiki"; | ||||
| import { nativeStyleToCss } from "yoshiki/native"; | ||||
| 
 | ||||
| // The blurhashToUrl has been stolen from https://gist.github.com/mattiaz9/53cb67040fa135cb395b1d015a200aff
 | ||||
| export function blurHashToDataURL(hash: string | undefined): string | undefined { | ||||
| 	if (!hash) return undefined; | ||||
| 
 | ||||
| 	const pixels = decode(hash, 32, 32); | ||||
| 	const dataURL = parsePixels(pixels, 32, 32); | ||||
| 	return dataURL; | ||||
| } | ||||
| 
 | ||||
| // thanks to https://github.com/wheany/js-png-encoder
 | ||||
| function parsePixels(pixels: Uint8ClampedArray, width: number, height: number) { | ||||
| 	const pixelsString = Array.from(pixels) | ||||
| 		.map((byte) => String.fromCharCode(byte)) | ||||
| 		.join(""); | ||||
| 	const pngString = generatePng(width, height, pixelsString); | ||||
| 	const dataURL = | ||||
| 		typeof Buffer !== "undefined" | ||||
| 			? Buffer.from(getPngArray(pngString)).toString("base64") | ||||
| 			: btoa(pngString); | ||||
| 	return `data:image/png;base64,${dataURL}`; | ||||
| } | ||||
| 
 | ||||
| function getPngArray(pngString: string) { | ||||
| 	const pngArray = new Uint8Array(pngString.length); | ||||
| 	for (let i = 0; i < pngString.length; i++) { | ||||
| 		pngArray[i] = pngString.charCodeAt(i); | ||||
| 	} | ||||
| 	return pngArray; | ||||
| } | ||||
| 
 | ||||
| function generatePng(width: number, height: number, rgbaString: string) { | ||||
| 	const DEFLATE_METHOD = String.fromCharCode(0x78, 0x01); | ||||
| 	const CRC_TABLE: number[] = []; | ||||
| 	const SIGNATURE = String.fromCharCode(137, 80, 78, 71, 13, 10, 26, 10); | ||||
| 	const NO_FILTER = String.fromCharCode(0); | ||||
| 
 | ||||
| 	// biome-ignore lint: not gonna fix stackowerflow code that works
 | ||||
| 	let n, c, k; | ||||
| 
 | ||||
| 	// make crc table
 | ||||
| 	for (n = 0; n < 256; n++) { | ||||
| 		c = n; | ||||
| 		for (k = 0; k < 8; k++) { | ||||
| 			if (c & 1) { | ||||
| 				c = 0xedb88320 ^ (c >>> 1); | ||||
| 			} else { | ||||
| 				c = c >>> 1; | ||||
| 			} | ||||
| 		} | ||||
| 		CRC_TABLE[n] = c; | ||||
| 	} | ||||
| 
 | ||||
| 	// Functions
 | ||||
| 	function inflateStore(data: string) { | ||||
| 		const MAX_STORE_LENGTH = 65535; | ||||
| 		let storeBuffer = ""; | ||||
| 		// biome-ignore lint: not gonna fix stackowerflow code that works
 | ||||
| 		let remaining; | ||||
| 		// biome-ignore lint: not gonna fix stackowerflow code that works
 | ||||
| 		let blockType; | ||||
| 
 | ||||
| 		for (let i = 0; i < data.length; i += MAX_STORE_LENGTH) { | ||||
| 			remaining = data.length - i; | ||||
| 			blockType = ""; | ||||
| 
 | ||||
| 			if (remaining <= MAX_STORE_LENGTH) { | ||||
| 				blockType = String.fromCharCode(0x01); | ||||
| 			} else { | ||||
| 				remaining = MAX_STORE_LENGTH; | ||||
| 				blockType = String.fromCharCode(0x00); | ||||
| 			} | ||||
| 			// little-endian
 | ||||
| 			storeBuffer += blockType + String.fromCharCode(remaining & 0xff, (remaining & 0xff00) >>> 8); | ||||
| 			storeBuffer += String.fromCharCode(~remaining & 0xff, (~remaining & 0xff00) >>> 8); | ||||
| 
 | ||||
| 			storeBuffer += data.substring(i, i + remaining); | ||||
| 		} | ||||
| 
 | ||||
| 		return storeBuffer; | ||||
| 	} | ||||
| 
 | ||||
| 	function adler32(data: string) { | ||||
| 		const MOD_ADLER = 65521; | ||||
| 		let a = 1; | ||||
| 		let b = 0; | ||||
| 
 | ||||
| 		for (let i = 0; i < data.length; i++) { | ||||
| 			a = (a + data.charCodeAt(i)) % MOD_ADLER; | ||||
| 			b = (b + a) % MOD_ADLER; | ||||
| 		} | ||||
| 
 | ||||
| 		return (b << 16) | a; | ||||
| 	} | ||||
| 
 | ||||
| 	function updateCrc(crc: number, buf: string) { | ||||
| 		let c = crc; | ||||
| 		let b: number; | ||||
| 
 | ||||
| 		for (let n = 0; n < buf.length; n++) { | ||||
| 			b = buf.charCodeAt(n); | ||||
| 			c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8); | ||||
| 		} | ||||
| 		return c; | ||||
| 	} | ||||
| 
 | ||||
| 	function crc(buf: string) { | ||||
| 		return updateCrc(0xffffffff, buf) ^ 0xffffffff; | ||||
| 	} | ||||
| 
 | ||||
| 	function dwordAsString(dword: number) { | ||||
| 		return String.fromCharCode( | ||||
| 			(dword & 0xff000000) >>> 24, | ||||
| 			(dword & 0x00ff0000) >>> 16, | ||||
| 			(dword & 0x0000ff00) >>> 8, | ||||
| 			dword & 0x000000ff, | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	function createChunk(length: number, type: string, data: string) { | ||||
| 		const CRC = crc(type + data); | ||||
| 
 | ||||
| 		return dwordAsString(length) + type + data + dwordAsString(CRC); | ||||
| 	} | ||||
| 
 | ||||
| 	function createIHDR(width: number, height: number) { | ||||
| 		const IHDRdata = | ||||
| 			dwordAsString(width) + | ||||
| 			dwordAsString(height) + | ||||
| 			// bit depth
 | ||||
| 			String.fromCharCode(8) + | ||||
| 			// color type: 6=truecolor with alpha
 | ||||
| 			String.fromCharCode(6) + | ||||
| 			// compression method: 0=deflate, only allowed value
 | ||||
| 			String.fromCharCode(0) + | ||||
| 			// filtering: 0=adaptive, only allowed value
 | ||||
| 			String.fromCharCode(0) + | ||||
| 			// interlacing: 0=none
 | ||||
| 			String.fromCharCode(0); | ||||
| 
 | ||||
| 		return createChunk(13, "IHDR", IHDRdata); | ||||
| 	} | ||||
| 
 | ||||
| 	// PNG creations
 | ||||
| 
 | ||||
| 	const IEND = createChunk(0, "IEND", ""); | ||||
| 	const IHDR = createIHDR(width, height); | ||||
| 
 | ||||
| 	let scanlines = ""; | ||||
| 	let scanline: string; | ||||
| 
 | ||||
| 	for (let y = 0; y < rgbaString.length; y += width * 4) { | ||||
| 		scanline = NO_FILTER; | ||||
| 		if (Array.isArray(rgbaString)) { | ||||
| 			for (let x = 0; x < width * 4; x++) { | ||||
| 				scanline += String.fromCharCode(rgbaString[y + x] & 0xff); | ||||
| 			} | ||||
| 		} else { | ||||
| 			scanline += rgbaString.substr(y, width * 4); | ||||
| 		} | ||||
| 		scanlines += scanline; | ||||
| 	} | ||||
| 
 | ||||
| 	const compressedScanlines = | ||||
| 		DEFLATE_METHOD + inflateStore(scanlines) + dwordAsString(adler32(scanlines)); | ||||
| 	const IDAT = createChunk(compressedScanlines.length, "IDAT", compressedScanlines); | ||||
| 
 | ||||
| 	const pngString = SIGNATURE + IHDR + IDAT + IEND; | ||||
| 	return pngString; | ||||
| } | ||||
| 
 | ||||
| const BlurhashCanvas = forwardRef< | ||||
| 	HTMLCanvasElement, | ||||
| 	{ | ||||
| 		blurhash: string; | ||||
| 	} & HTMLAttributes<HTMLCanvasElement> | ||||
| >(function BlurhashCanvas({ blurhash, ...props }, forwardedRef) { | ||||
| 	const ref = useRef<HTMLCanvasElement>(null); | ||||
| 	const { css } = useYoshiki(); | ||||
| 
 | ||||
| 	useImperativeHandle(forwardedRef, () => ref.current!, []); | ||||
| 
 | ||||
| 	useLayoutEffect(() => { | ||||
| 		if (!ref.current) return; | ||||
| 		const pixels = decode(blurhash, 32, 32); | ||||
| 		const ctx = ref.current.getContext("2d"); | ||||
| 		if (!ctx) return; | ||||
| 		const imageData = ctx.createImageData(32, 32); | ||||
| 		imageData.data.set(pixels); | ||||
| 		ctx.putImageData(imageData, 0, 0); | ||||
| 	}, [blurhash]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<canvas | ||||
| 			ref={ref} | ||||
| 			width={32} | ||||
| 			height={32} | ||||
| 			{...css( | ||||
| 				{ | ||||
| 					position: "absolute", | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 					left: 0, | ||||
| 					right: 0, | ||||
| 					width: "100%", | ||||
| 					height: "100%", | ||||
| 				}, | ||||
| 				props, | ||||
| 			)} | ||||
| 		/> | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| const BlurhashDiv = forwardRef< | ||||
| 	HTMLDivElement, | ||||
| 	{ blurhash: string } & HTMLAttributes<HTMLDivElement> | ||||
| >(function BlurhashDiv({ blurhash, ...props }, ref) { | ||||
| 	const { css } = useYoshiki(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<div | ||||
| 			ref={ref} | ||||
| 			style={{ | ||||
| 				// Use a blurhash here to nicely fade the NextImage when it is loaded completly
 | ||||
| 				// (this prevents loading the image line by line which is ugly and buggy on firefox)
 | ||||
| 				backgroundImage: `url(${blurHashToDataURL(blurhash)})`, | ||||
| 				backgroundSize: "cover", | ||||
| 				backgroundRepeat: "no-repeat", | ||||
| 				backgroundPosition: "50% 50%", | ||||
| 			}} | ||||
| 			{...css( | ||||
| 				{ | ||||
| 					position: "absolute", | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 					left: 0, | ||||
| 					right: 0, | ||||
| 					width: "100%", | ||||
| 					height: "100%", | ||||
| 				}, | ||||
| 				props, | ||||
| 			)} | ||||
| 		/> | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| export const useRenderType = () => { | ||||
| 	const [renderType, setRenderType] = useState<"ssr" | "hydratation" | "client">( | ||||
| 		typeof window === "undefined" ? "ssr" : "hydratation", | ||||
| 	); | ||||
| 
 | ||||
| 	useLayoutEffect(() => { | ||||
| 		setRenderType("client"); | ||||
| 	}, []); | ||||
| 
 | ||||
| 	return renderType; | ||||
| }; | ||||
| 
 | ||||
| export const BlurhashContainer = ({ | ||||
| 	blurhash, | ||||
| 	children, | ||||
| 	...props | ||||
| }: { | ||||
| 	blurhash: string; | ||||
| 	children?: ReactElement | ReactElement[]; | ||||
| }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 	const renderType = useRenderType(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<div | ||||
| 			{...css( | ||||
| 				{ | ||||
| 					// To reproduce view's behavior
 | ||||
| 					boxSizing: "border-box", | ||||
| 					overflow: "hidden", | ||||
| 					position: "relative", | ||||
| 				}, | ||||
| 				nativeStyleToCss(props), | ||||
| 			)} | ||||
| 		> | ||||
| 			{renderType === "ssr" && <BlurhashDiv blurhash={blurhash} />} | ||||
| 			{renderType === "client" && <BlurhashCanvas blurhash={blurhash} />} | ||||
| 			{renderType === "hydratation" && ( | ||||
| 				<div dangerouslySetInnerHTML={{ __html: "" }} suppressHydrationWarning /> | ||||
| 			)} | ||||
| 			{children} | ||||
| 		</div> | ||||
| 	); | ||||
| }; | ||||
| @ -1,102 +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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import { getCurrentToken } from "@kyoo/models"; | ||||
| import { type ReactElement, useState } from "react"; | ||||
| import { type FlexStyle, type ImageStyle, View, type ViewStyle } from "react-native"; | ||||
| import { Blurhash } from "react-native-blurhash"; | ||||
| import FastImage from "react-native-fast-image"; | ||||
| import { percent, useYoshiki } from "yoshiki/native"; | ||||
| import { Skeleton } from "../skeleton"; | ||||
| import type { ImageLayout, Props } from "./base-image"; | ||||
| 
 | ||||
| export const Image = ({ | ||||
| 	src, | ||||
| 	quality, | ||||
| 	alt, | ||||
| 	forcedLoading = false, | ||||
| 	layout, | ||||
| 	Err, | ||||
| 	...props | ||||
| }: Props & { style?: ImageStyle } & { layout: ImageLayout }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 	const [state, setState] = useState<"loading" | "errored" | "finished">( | ||||
| 		src ? "loading" : "errored", | ||||
| 	); | ||||
| 
 | ||||
| 	// This could be done with a key but this makes the API easier to use.
 | ||||
| 	// This unsures that the state is resetted when the source change (useful for recycler lists.)
 | ||||
| 	const [oldSource, setOldSource] = useState(src); | ||||
| 	if (oldSource !== src) { | ||||
| 		setState("loading"); | ||||
| 		setOldSource(src); | ||||
| 	} | ||||
| 
 | ||||
| 	const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle; | ||||
| 
 | ||||
| 	if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />; | ||||
| 	if (!src || state === "errored") { | ||||
| 		return Err !== undefined ? ( | ||||
| 			Err | ||||
| 		) : ( | ||||
| 			<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} /> | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	quality ??= "high"; | ||||
| 	const token = getCurrentToken(); | ||||
| 	return ( | ||||
| 		<View {...css([layout, border], props)}> | ||||
| 			{state !== "finished" && ( | ||||
| 				<Blurhash | ||||
| 					blurhash={src.blurhash} | ||||
| 					resizeMode="cover" | ||||
| 					{...css({ width: percent(100), height: percent(100) })} | ||||
| 				/> | ||||
| 			)} | ||||
| 			<FastImage | ||||
| 				source={{ | ||||
| 					uri: src[quality], | ||||
| 					headers: token | ||||
| 						? { | ||||
| 								Authorization: token, | ||||
| 							} | ||||
| 						: {}, | ||||
| 					priority: FastImage.priority[quality === "medium" ? "normal" : quality], | ||||
| 				}} | ||||
| 				accessibilityLabel={alt} | ||||
| 				onLoad={() => setState("finished")} | ||||
| 				onError={() => setState("errored")} | ||||
| 				resizeMode={FastImage.resizeMode.cover} | ||||
| 				{...(css({ | ||||
| 					width: percent(100), | ||||
| 					height: percent(100), | ||||
| 				}) as { style: FlexStyle })} | ||||
| 			/> | ||||
| 		</View> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| Image.Loader = ({ layout, ...props }: { layout: ImageLayout; children?: ReactElement }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 	const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle; | ||||
| 
 | ||||
| 	return <Skeleton variant="custom" show {...css([layout, border], props)} />; | ||||
| }; | ||||
| @ -1,89 +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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import NextImage from "next/image"; | ||||
| import { type ReactElement, useState } from "react"; | ||||
| import { type ImageStyle, View, type ViewStyle } from "react-native"; | ||||
| import { useYoshiki } from "yoshiki/native"; | ||||
| import { imageBorderRadius } from "../constants"; | ||||
| import { Skeleton } from "../skeleton"; | ||||
| import type { ImageLayout, Props } from "./base-image"; | ||||
| import { BlurhashContainer, useRenderType } from "./blurhash.web"; | ||||
| 
 | ||||
| export const Image = ({ | ||||
| 	src, | ||||
| 	quality, | ||||
| 	alt, | ||||
| 	forcedLoading = false, | ||||
| 	layout, | ||||
| 	Err, | ||||
| 	...props | ||||
| }: Props & { style?: ImageStyle } & { layout: ImageLayout }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 	const [state, setState] = useState<"loading" | "errored" | "finished">( | ||||
| 		typeof window === "undefined" ? "finished" : "loading", | ||||
| 	); | ||||
| 
 | ||||
| 	const border = { borderRadius: imageBorderRadius } satisfies ViewStyle; | ||||
| 
 | ||||
| 	if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />; | ||||
| 	if (!src || state === "errored") { | ||||
| 		return Err !== undefined ? ( | ||||
| 			Err | ||||
| 		) : ( | ||||
| 			<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} /> | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	return ( | ||||
| 		<BlurhashContainer blurhash={src.blurhash} {...css([layout, border], props)}> | ||||
| 			<img | ||||
| 				style={{ | ||||
| 					position: "absolute", | ||||
| 					inset: 0, | ||||
| 					width: "100%", | ||||
| 					height: "100%", | ||||
| 					color: "transparent", | ||||
| 					objectFit: "cover", | ||||
| 					opacity: state === "loading" ? 0 : 1, | ||||
| 					transition: "opacity .2s ease-out", | ||||
| 				}} | ||||
| 				// It's intended to keep `loading` before `src` because React updates
 | ||||
| 				// props in order which causes Safari/Firefox to not lazy load properly.
 | ||||
| 				// See https://github.com/facebook/react/issues/25883
 | ||||
| 				loading={quality === "high" ? "eager" : "lazy"} | ||||
| 				decoding="async" | ||||
| 				fetchpriority={quality === "high" ? "high" : undefined} | ||||
| 				src={src[quality ?? "high"]} | ||||
| 				alt={alt!} | ||||
| 				onLoad={() => setState("finished")} | ||||
| 				onError={() => setState("errored")} | ||||
| 				suppressHydrationWarning | ||||
| 			/> | ||||
| 		</BlurhashContainer> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| Image.Loader = ({ layout, ...props }: { layout: ImageLayout; children?: ReactElement }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 	const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle; | ||||
| 
 | ||||
| 	return <Skeleton variant="custom" show {...css([layout, border], props)} />; | ||||
| }; | ||||
| @ -1,170 +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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import { LinearGradient, type LinearGradientProps } from "expo-linear-gradient"; | ||||
| import type { ComponentProps, ComponentType, ReactElement, ReactNode } from "react"; | ||||
| import { type ImageStyle, View, type ViewProps, type ViewStyle } from "react-native"; | ||||
| import { percent } from "yoshiki/native"; | ||||
| import { useYoshiki } from "yoshiki/native"; | ||||
| import { imageBorderRadius } from "../constants"; | ||||
| import { ContrastArea } from "../themes"; | ||||
| import type { ImageLayout, Props, YoshikiEnhanced } from "./base-image"; | ||||
| import { Image } from "./image"; | ||||
| 
 | ||||
| export { Sprite } from "./sprite"; | ||||
| export { BlurhashContainer } from "./blurhash"; | ||||
| export { type Props as ImageProps, Image }; | ||||
| 
 | ||||
| export const Poster = ({ | ||||
| 	alt, | ||||
| 	layout, | ||||
| 	...props | ||||
| }: Props & { style?: ImageStyle } & { | ||||
| 	layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; | ||||
| }) => <Image alt={alt!} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />; | ||||
| 
 | ||||
| Poster.Loader = ({ | ||||
| 	layout, | ||||
| 	...props | ||||
| }: { | ||||
| 	children?: ReactElement; | ||||
| 	layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; | ||||
| }) => <Image.Loader layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />; | ||||
| 
 | ||||
| export const PosterBackground = ({ | ||||
| 	alt, | ||||
| 	layout, | ||||
| 	...props | ||||
| }: Omit<ComponentProps<typeof ImageBackground>, "layout"> & { style?: ImageStyle } & { | ||||
| 	layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; | ||||
| }) => { | ||||
| 	const { css } = useYoshiki(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<ImageBackground | ||||
| 			alt={alt!} | ||||
| 			layout={{ aspectRatio: 2 / 3, ...layout }} | ||||
| 			{...css({ borderRadius: imageBorderRadius }, props)} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type ImageBackgroundProps = { | ||||
| 	children?: ReactNode; | ||||
| 	containerStyle?: YoshikiEnhanced<ViewStyle>; | ||||
| 	imageStyle?: YoshikiEnhanced<ImageStyle>; | ||||
| 	layout?: ImageLayout; | ||||
| 	contrast?: "light" | "dark" | "user"; | ||||
| }; | ||||
| 
 | ||||
| export const ImageBackground = <AsProps = ViewProps>({ | ||||
| 	src, | ||||
| 	alt, | ||||
| 	quality, | ||||
| 	as, | ||||
| 	children, | ||||
| 	containerStyle, | ||||
| 	imageStyle, | ||||
| 	layout, | ||||
| 	contrast = "dark", | ||||
| 	imageSibling, | ||||
| 	...asProps | ||||
| }: { | ||||
| 	as?: ComponentType<AsProps>; | ||||
| 	imageSibling?: ReactElement; | ||||
| } & AsProps & | ||||
| 	ImageBackgroundProps & | ||||
| 	Props) => { | ||||
| 	const Container = as ?? View; | ||||
| 
 | ||||
| 	return ( | ||||
| 		<ContrastArea contrastText mode={contrast}> | ||||
| 			{({ css }) => ( | ||||
| 				<Container {...(css([layout, { overflow: "hidden" }], asProps) as AsProps)}> | ||||
| 					<View | ||||
| 						{...css([ | ||||
| 							{ | ||||
| 								position: "absolute", | ||||
| 								top: 0, | ||||
| 								bottom: 0, | ||||
| 								left: 0, | ||||
| 								right: 0, | ||||
| 								zIndex: -1, | ||||
| 								bg: (theme) => theme.background, | ||||
| 							}, | ||||
| 							containerStyle, | ||||
| 						])} | ||||
| 					> | ||||
| 						{src && ( | ||||
| 							<Image | ||||
| 								src={src} | ||||
| 								quality={quality} | ||||
| 								alt={alt!} | ||||
| 								layout={{ width: percent(100), height: percent(100) }} | ||||
| 								{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as { | ||||
| 									style: ImageStyle; | ||||
| 								})} | ||||
| 							/> | ||||
| 						)} | ||||
| 						{imageSibling} | ||||
| 					</View> | ||||
| 					{children} | ||||
| 				</Container> | ||||
| 			)} | ||||
| 		</ContrastArea> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| export const GradientImageBackground = <AsProps = ViewProps>({ | ||||
| 	contrast = "dark", | ||||
| 	gradient, | ||||
| 	...props | ||||
| }: { | ||||
| 	as?: ComponentType<AsProps>; | ||||
| 	gradient?: Partial<LinearGradientProps>; | ||||
| } & AsProps & | ||||
| 	ImageBackgroundProps & | ||||
| 	Props) => { | ||||
| 	const { css, theme } = useYoshiki(); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<ImageBackground | ||||
| 			contrast={contrast} | ||||
| 			imageSibling={ | ||||
| 				<LinearGradient | ||||
| 					start={{ x: 0, y: 0.25 }} | ||||
| 					end={{ x: 0, y: 1 }} | ||||
| 					colors={["transparent", theme[contrast].darkOverlay]} | ||||
| 					{...css( | ||||
| 						{ | ||||
| 							position: "absolute", | ||||
| 							top: 0, | ||||
| 							bottom: 0, | ||||
| 							left: 0, | ||||
| 							right: 0, | ||||
| 						}, | ||||
| 						typeof gradient === "object" ? gradient : undefined, | ||||
| 					)} | ||||
| 				/> | ||||
| 			} | ||||
| 			{...(props as any)} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| @ -1,56 +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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import NextImage from "next/image"; | ||||
| 
 | ||||
| export const Sprite = ({ | ||||
| 	src, | ||||
| 	alt, | ||||
| 	style, | ||||
| 	x, | ||||
| 	y, | ||||
| 	...props | ||||
| }: { | ||||
| 	src: string; | ||||
| 	alt: string; | ||||
| 	style?: object; | ||||
| 	width: number; | ||||
| 	height: number; | ||||
| 	x: number; | ||||
| 	y: number; | ||||
| }) => { | ||||
| 	return ( | ||||
| 		<NextImage | ||||
| 			src={src} | ||||
| 			priority={false} | ||||
| 			alt={alt!} | ||||
| 			// Don't use next's server to reprocess images, they are already optimized by kyoo.
 | ||||
| 			unoptimized={true} | ||||
| 			style={{ | ||||
| 				objectFit: "none", | ||||
| 				objectPosition: `${-x}px ${-y}px`, | ||||
| 				flexGrow: 0, | ||||
| 				flexShrink: 0, | ||||
| 				...style, | ||||
| 			}} | ||||
| 			{...props} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| @ -4,8 +4,9 @@ export * from "./theme"; | ||||
| export * from "./icons"; | ||||
| export * from "./links"; | ||||
| export * from "./avatar"; | ||||
| // export * from "./image";
 | ||||
| // export * from "./skeleton";
 | ||||
| export * from "./image"; | ||||
| export * from "./image-background"; | ||||
| export * from "./skeleton"; | ||||
| export * from "./tooltip"; | ||||
| // export * from "./container";
 | ||||
| export * from "./divider"; | ||||
| @ -21,4 +22,3 @@ export * from "./button"; | ||||
| // export * from "./chip";
 | ||||
| 
 | ||||
| export * from "./utils"; | ||||
| export * from "./constants"; | ||||
|  | ||||
| @ -1,23 +1,3 @@ | ||||
| /* | ||||
|  * Kyoo - A portable and vast media library solution. | ||||
|  * Copyright (c) Kyoo. | ||||
|  * | ||||
|  * See AUTHORS.md and LICENSE file in the project root for full license information. | ||||
|  * | ||||
|  * Kyoo is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * any later version. | ||||
|  * | ||||
|  * Kyoo is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| import { LinearGradient as LG } from "expo-linear-gradient"; | ||||
| import { useEffect } from "react"; | ||||
| import { StyleSheet, View, type ViewProps } from "react-native"; | ||||
| @ -32,31 +12,11 @@ import { em, percent, px, rem, useYoshiki } from "yoshiki/native"; | ||||
| 
 | ||||
| const LinearGradient = Animated.createAnimatedComponent(LG); | ||||
| 
 | ||||
| export const SkeletonCss = () => ( | ||||
| 	<style jsx global>{` | ||||
| 		@keyframes skeleton { | ||||
| 			0% { | ||||
| 				transform: translateX(-100%); | ||||
| 			} | ||||
| 			50% { | ||||
| 				transform: translateX(100%); | ||||
| 			} | ||||
| 			100% { | ||||
| 				transform: translateX(100%); | ||||
| 			} | ||||
| 		} | ||||
| 	`}</style>
 | ||||
| ); | ||||
| 
 | ||||
| export const Skeleton = ({ | ||||
| 	children, | ||||
| 	show: forcedShow, | ||||
| 	lines = 1, | ||||
| 	variant = "text", | ||||
| 	...props | ||||
| }: Omit<ViewProps, "children"> & { | ||||
| 	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 ( | ||||
| 		<View | ||||
| 			{...css( | ||||
| @ -115,46 +73,44 @@ export const Skeleton = ({ | ||||
| 				props, | ||||
| 			)} | ||||
| 		> | ||||
| 			{(forcedShow || !children || children === true) && | ||||
| 				[...Array(lines)].map((_, i) => ( | ||||
| 					<View | ||||
| 						key={`skeleton_${i}`} | ||||
| 						onLayout={(e) => { | ||||
| 							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), | ||||
| 							}, | ||||
| 						])} | ||||
| 					> | ||||
| 						<LinearGradient | ||||
| 							start={{ x: 0, y: 0.5 }} | ||||
| 							end={{ x: 1, y: 0.5 }} | ||||
| 							colors={["transparent", theme.overlay1, "transparent"]} | ||||
| 							style={[ | ||||
| 								StyleSheet.absoluteFillObject, | ||||
| 								{ transform: [{ translateX: -width.value }] }, | ||||
| 								animated, | ||||
| 							]} | ||||
| 						/> | ||||
| 					</View> | ||||
| 				))} | ||||
| 			{children} | ||||
| 			{[...Array(lines)].map((_, i) => ( | ||||
| 				<View | ||||
| 					key={`skeleton_${i}`} | ||||
| 					onLayout={(e) => { | ||||
| 						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), | ||||
| 						}, | ||||
| 					])} | ||||
| 				> | ||||
| 					<LinearGradient | ||||
| 						start={{ x: 0, y: 0.5 }} | ||||
| 						end={{ x: 1, y: 0.5 }} | ||||
| 						colors={["transparent", theme.overlay1, "transparent"]} | ||||
| 						style={[ | ||||
| 							StyleSheet.absoluteFillObject, | ||||
| 							{ transform: [{ translateX: -width.value }] }, | ||||
| 							animated, | ||||
| 						]} | ||||
| 					/> | ||||
| 				</View> | ||||
| 			))} | ||||
| 		</View> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| @ -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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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 = () => ( | ||||
| 	<style jsx global>{` | ||||
| 		@keyframes skeleton { | ||||
| 			0% { | ||||
| 				transform: translateX(-100%); | ||||
| 			} | ||||
| 			50% { | ||||
| 				transform: translateX(100%); | ||||
| 			} | ||||
| 			100% { | ||||
| 				transform: translateX(100%); | ||||
| 			} | ||||
| 		} | ||||
| 	`}</style>
 | ||||
| ); | ||||
| 
 | ||||
| export const Skeleton = ({ | ||||
| 	children, | ||||
| 	show: forcedShow, | ||||
| 	lines = 1, | ||||
| 	variant = "text", | ||||
| 	...props | ||||
| }: Omit<ViewProps, "children"> & { | ||||
| 	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 ( | ||||
| 		<View | ||||
| 			{...css( | ||||
| 				[ | ||||
| 					lines === 1 && { overflow: "hidden", borderRadius: px(6) }, | ||||
| 					(variant === "text" || variant === "header") && | ||||
| 						lines === 1 && [ | ||||
| 							{ | ||||
| 								width: percent(75), | ||||
| 								height: rem(1.2), | ||||
| 								margin: px(2), | ||||
| 							}, | ||||
| 							variant === "text" && { | ||||
| 								margin: px(2), | ||||
| 							}, | ||||
| 							variant === "header" && { | ||||
| 								marginBottom: rem(0.5), | ||||
| 							}, | ||||
| 						], | ||||
| 
 | ||||
| 					variant === "round" && { | ||||
| 						borderRadius: 9999999, | ||||
| 					}, | ||||
| 					variant === "fill" && { | ||||
| 						width: percent(100), | ||||
| 						height: percent(100), | ||||
| 					}, | ||||
| 					variant === "filltext" && { | ||||
| 						width: percent(100), | ||||
| 						height: em(1), | ||||
| 					}, | ||||
| 				], | ||||
| 				props, | ||||
| 			)} | ||||
| 		> | ||||
| 			{(forcedShow || !children || children === true) && | ||||
| 				[...Array(lines)].map((_, i) => ( | ||||
| 					<View | ||||
| 						key={`skeleton_${i}`} | ||||
| 						{...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), | ||||
| 							}, | ||||
| 						])} | ||||
| 					> | ||||
| 						<LinearGradient | ||||
| 							start={{ x: 0, y: 0.5 }} | ||||
| 							end={{ x: 1, y: 0.5 }} | ||||
| 							colors={["transparent", theme.overlay1, "transparent"]} | ||||
| 							{...css([ | ||||
| 								{ | ||||
| 									position: "absolute", | ||||
| 									top: 0, | ||||
| 									bottom: 0, | ||||
| 									left: 0, | ||||
| 									right: 0, | ||||
| 									// @ts-ignore Web only properties
 | ||||
| 									animation: "skeleton 1.6s linear 0.5s infinite", | ||||
| 									transform: "translateX(-100%)", | ||||
| 								}, | ||||
| 							])} | ||||
| 						/> | ||||
| 					</View> | ||||
| 				))} | ||||
| 			{children} | ||||
| 		</View> | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										57
									
								
								front/src/primitives/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										57
									
								
								front/src/primitives/types.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<string, string>; | ||||
| 		hrefAttrs?: { | ||||
| 			rel: "noreferrer"; | ||||
| 			target?: "_blank"; | ||||
| 		}; | ||||
| 		onClick?: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| declare module "react" { | ||||
| 	interface StyleHTMLAttributes<T> extends HTMLAttributes<T> { | ||||
| 		jsx?: boolean; | ||||
| 		global?: boolean; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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"; | ||||
| 
 | ||||
|  | ||||
| @ -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 = <T extends ZodTypeAny>(key: string, parser: T) => { | ||||
| export const readCookie = <T extends ZodType>(key: string, parser: T) => { | ||||
| 	const cookies = getServerData("cookies"); | ||||
| 	console.log("cookies", cookies); | ||||
| 	const decodedCookie = decodeURIComponent(cookies); | ||||
| @ -37,7 +37,7 @@ export const readCookie = <T extends ZodTypeAny>(key: string, parser: T) => { | ||||
| 	return parser.parse(JSON.parse(str)) as z.infer<T>; | ||||
| }; | ||||
| 
 | ||||
| export const useStoreValue = <T extends ZodTypeAny>(key: string, parser: T) => { | ||||
| export const useStoreValue = <T extends ZodType>(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 = <T extends ZodTypeAny>(key: string, parser: T) => { | ||||
| export const readValue = <T extends ZodType>(key: string, parser: T) => { | ||||
| 	if (Platform.OS === "web" && typeof window === "undefined") { | ||||
| 		return readCookie(key, parser); | ||||
| 	} | ||||
|  | ||||
| @ -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 <Parser extends z.ZodTypeAny>(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<string, any>; | ||||
| 	try { | ||||
| @ -82,7 +82,7 @@ const queryFn = async <Parser extends z.ZodTypeAny>(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<T = unknown, Ret = T> = { | ||||
| 	parser: z.ZodType<T, z.ZodTypeDef, any>; | ||||
| export type QueryIdentifier<T = unknown> = { | ||||
| 	parser: z.ZodType<T>; | ||||
| 	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<typeof toQueryKey>) => { | ||||
| }; | ||||
| 
 | ||||
| export const useFetch = <Data,>(query: QueryIdentifier<Data>) => { | ||||
| 	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<Data, KyooError>({ | ||||
| @ -155,17 +156,18 @@ export const useFetch = <Data,>(query: QueryIdentifier<Data>) => { | ||||
| 				signal: ctx.signal, | ||||
| 				authToken: authToken ?? null, | ||||
| 				...query.options, | ||||
| 			}), | ||||
| 			}) as Promise<Data>, | ||||
| 		placeholderData: query.placeholderData as any, | ||||
| 		enabled: query.enabled, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => { | ||||
| 	const { apiUrl, authToken } = useContext(AccountContext); | ||||
| export const useInfiniteFetch = <Data,>(query: QueryIdentifier<Data>) => { | ||||
| 	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<Page<Data>, KyooError>({ | ||||
| 	const res = useInfiniteQuery<Page<Data>, KyooError>({ | ||||
| 		queryKey: key, | ||||
| 		queryFn: (ctx) => | ||||
| 			queryFn({ | ||||
| @ -174,20 +176,15 @@ export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) = | ||||
| 				signal: ctx.signal, | ||||
| 				authToken: authToken ?? null, | ||||
| 				...query.options, | ||||
| 			}), | ||||
| 			}) as Promise<Page<Data>>, | ||||
| 		getNextPageParam: (page: Page<Data>) => 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 = <T = void,>({ | ||||
| export const useMutation = <T = void>({ | ||||
| 	compute, | ||||
| 	invalidate, | ||||
| 	...queryParams | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user