mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -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 { useTranslation } from "react-i18next";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import type { Serie } from "~/models";
|
import { WatchStatusV } from "~/models";
|
||||||
import { HR, IconButton, Menu, tooltip } from "~/primitives";
|
import { HR, IconButton, Menu, tooltip } from "~/primitives";
|
||||||
import { useAccount } from "~/providers/account-context";
|
import { useAccount } from "~/providers/account-context";
|
||||||
import { useMutation } from "~/query";
|
import { useMutation } from "~/query";
|
||||||
import { watchListIcon } from "./watchlist-info";
|
import { watchListIcon } from "./watchlist-info";
|
||||||
// import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads";
|
// import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads";
|
||||||
|
|
||||||
type WatchStatusV = NonNullable<Serie["watchStatus"]>["status"];
|
|
||||||
|
|
||||||
export const EpisodesContext = ({
|
export const EpisodesContext = ({
|
||||||
type = "episode",
|
type = "episode",
|
||||||
slug,
|
slug,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { type ImageStyle, Platform, View } from "react-native";
|
import { type ImageStyle, Platform, View } from "react-native";
|
||||||
import { type Stylable, type Theme, percent, px, useYoshiki } from "yoshiki/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 {
|
import {
|
||||||
Link,
|
Link,
|
||||||
P,
|
P,
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
} from "~/primitives";
|
} from "~/primitives";
|
||||||
import type { Layout } from "~/query";
|
import type { Layout } from "~/query";
|
||||||
import { ItemWatchStatus } from "./item-helpers";
|
import { ItemWatchStatus } from "./item-helpers";
|
||||||
|
import { ItemContext } from "./context-menus";
|
||||||
|
|
||||||
export const ItemProgress = ({ watchPercent }: { watchPercent: number }) => {
|
export const ItemProgress = ({ watchPercent }: { watchPercent: number }) => {
|
||||||
const { css } = useYoshiki("episodebox");
|
const { css } = useYoshiki("episodebox");
|
||||||
@ -59,7 +60,7 @@ export const ItemGrid = ({
|
|||||||
slug: string;
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
subtitle: string | null;
|
subtitle: string | null;
|
||||||
poster: KyooImage | null;
|
poster: KImage | null;
|
||||||
watchStatus: WatchStatusV | null;
|
watchStatus: WatchStatusV | null;
|
||||||
watchPercent: number | null;
|
watchPercent: number | null;
|
||||||
type: "movie" | "serie" | "collection";
|
type: "movie" | "serie" | "collection";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { Genre } from "./utils/genre";
|
import { Genre } from "./utils/genre";
|
||||||
import { Image } from "./utils/images";
|
import { KImage } from "./utils/images";
|
||||||
import { Metadata } from "./utils/metadata";
|
import { Metadata } from "./utils/metadata";
|
||||||
import { zdate } from "./utils/utils";
|
import { zdate } from "./utils/utils";
|
||||||
|
|
||||||
@ -24,10 +24,10 @@ export const Collection = z
|
|||||||
genres: z.array(Genre),
|
genres: z.array(Genre),
|
||||||
externalId: Metadata,
|
externalId: Metadata,
|
||||||
|
|
||||||
poster: Image.nullable(),
|
poster: KImage.nullable(),
|
||||||
thumbnail: Image.nullable(),
|
thumbnail: KImage.nullable(),
|
||||||
banner: Image.nullable(),
|
banner: KImage.nullable(),
|
||||||
logo: Image.nullable(),
|
logo: KImage.nullable(),
|
||||||
|
|
||||||
createdAt: zdate(),
|
createdAt: zdate(),
|
||||||
updatedAt: zdate(),
|
updatedAt: zdate(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import z from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const Entry = z.object({
|
export const Entry = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
@ -8,3 +8,5 @@ export * from "./show";
|
|||||||
export * from "./entry";
|
export * from "./entry";
|
||||||
export * from "./studio";
|
export * from "./studio";
|
||||||
export * from "./video";
|
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 { Studio } from "./studio";
|
||||||
import { Genre } from "./utils/genre";
|
import { Genre } from "./utils/genre";
|
||||||
import { Image } from "./utils/images";
|
import { KImage } from "./utils/images";
|
||||||
import { Metadata } from "./utils/metadata";
|
import { Metadata } from "./utils/metadata";
|
||||||
import { zdate } from "./utils/utils";
|
import { zdate } from "./utils/utils";
|
||||||
import { EmbeddedVideo } from "./video";
|
import { EmbeddedVideo } from "./video";
|
||||||
@ -27,10 +27,10 @@ export const Movie = z
|
|||||||
genres: z.array(Genre),
|
genres: z.array(Genre),
|
||||||
externalId: Metadata,
|
externalId: Metadata,
|
||||||
|
|
||||||
poster: Image.nullable(),
|
poster: KImage.nullable(),
|
||||||
thumbnail: Image.nullable(),
|
thumbnail: KImage.nullable(),
|
||||||
banner: Image.nullable(),
|
banner: KImage.nullable(),
|
||||||
logo: Image.nullable(),
|
logo: KImage.nullable(),
|
||||||
trailerUrl: z.string().optional().nullable(),
|
trailerUrl: z.string().optional().nullable(),
|
||||||
|
|
||||||
isAvailable: z.boolean(),
|
isAvailable: z.boolean(),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { Entry } from "./entry";
|
import { Entry } from "./entry";
|
||||||
import { Studio } from "./studio";
|
import { Studio } from "./studio";
|
||||||
import { Genre } from "./utils/genre";
|
import { Genre } from "./utils/genre";
|
||||||
import { Image } from "./utils/images";
|
import { KImage } from "./utils/images";
|
||||||
import { Metadata } from "./utils/metadata";
|
import { Metadata } from "./utils/metadata";
|
||||||
import { zdate } from "./utils/utils";
|
import { zdate } from "./utils/utils";
|
||||||
|
|
||||||
@ -28,10 +28,10 @@ export const Serie = z
|
|||||||
runtime: z.number().nullable(),
|
runtime: z.number().nullable(),
|
||||||
externalId: Metadata,
|
externalId: Metadata,
|
||||||
|
|
||||||
poster: Image.nullable(),
|
poster: KImage.nullable(),
|
||||||
thumbnail: Image.nullable(),
|
thumbnail: KImage.nullable(),
|
||||||
banner: Image.nullable(),
|
banner: KImage.nullable(),
|
||||||
logo: Image.nullable(),
|
logo: KImage.nullable(),
|
||||||
trailerUrl: z.string().optional().nullable(),
|
trailerUrl: z.string().optional().nullable(),
|
||||||
|
|
||||||
entriesCount: z.number().int(),
|
entriesCount: z.number().int(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import z from "zod";
|
import { z } from "zod/v4";
|
||||||
import { Collection } from "./collection";
|
import { Collection } from "./collection";
|
||||||
import { Movie } from "./movie";
|
import { Movie } from "./movie";
|
||||||
import { Serie } from "./serie";
|
import { Serie } from "./serie";
|
||||||
@ -9,3 +9,6 @@ export const Show = z.union([
|
|||||||
Collection.and(z.object({ kind: z.literal("collection") })),
|
Collection.and(z.object({ kind: z.literal("collection") })),
|
||||||
]);
|
]);
|
||||||
export type Show = z.infer<typeof Show>;
|
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 { z } from "zod/v4";
|
||||||
import { Image } from "./utils/images";
|
import { KImage } from "./utils/images";
|
||||||
import { Metadata } from "./utils/metadata";
|
import { Metadata } from "./utils/metadata";
|
||||||
import { zdate } from "./utils/utils";
|
import { zdate } from "./utils/utils";
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ export const Studio = z.object({
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
logo: Image.nullable(),
|
logo: KImage.nullable(),
|
||||||
externalId: Metadata,
|
externalId: Metadata,
|
||||||
createdAt: zdate(),
|
createdAt: zdate(),
|
||||||
updatedAt: zdate(),
|
updatedAt: zdate(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
|
|
||||||
export const Genre = z.enum([
|
export const Genre = z.enum([
|
||||||
"action",
|
"action",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const Image = z
|
export const KImage = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
source: z.string(),
|
source: z.string(),
|
||||||
@ -13,4 +13,4 @@ export const Image = z
|
|||||||
high: `/images/${x.id}?quality=high`,
|
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(
|
export const Metadata = z.record(
|
||||||
|
z.string(),
|
||||||
z.object({
|
z.object({
|
||||||
dataId: z.string(),
|
dataId: z.string(),
|
||||||
link: z.string().nullable(),
|
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;
|
this: string;
|
||||||
first: string;
|
|
||||||
next: string | null;
|
next: string | null;
|
||||||
count: number;
|
|
||||||
items: T[];
|
items: T[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Paged = <Item>(item: z.ZodType<Item>): z.ZodSchema<Page<Item>> =>
|
export const Paged = <Parser extends ZodType>(ItemParser: Parser) =>
|
||||||
z.object({
|
z.object({
|
||||||
this: z.string(),
|
this: z.string(),
|
||||||
first: z.string(),
|
|
||||||
next: z.string().nullable(),
|
next: z.string().nullable(),
|
||||||
count: z.number(),
|
items: z.array(ItemParser),
|
||||||
items: z.array(item),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isPage = <T = unknown>(obj: unknown): obj is Page<T> =>
|
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;
|
export const zdate = z.coerce.date;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const EmbeddedVideo = z.object({
|
export const EmbeddedVideo = z.object({
|
||||||
id: z.string(),
|
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 "./icons";
|
||||||
export * from "./links";
|
export * from "./links";
|
||||||
export * from "./avatar";
|
export * from "./avatar";
|
||||||
// export * from "./image";
|
export * from "./image";
|
||||||
// export * from "./skeleton";
|
export * from "./image-background";
|
||||||
|
export * from "./skeleton";
|
||||||
export * from "./tooltip";
|
export * from "./tooltip";
|
||||||
// export * from "./container";
|
// export * from "./container";
|
||||||
export * from "./divider";
|
export * from "./divider";
|
||||||
@ -21,4 +22,3 @@ export * from "./button";
|
|||||||
// export * from "./chip";
|
// export * from "./chip";
|
||||||
|
|
||||||
export * from "./utils";
|
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 { LinearGradient as LG } from "expo-linear-gradient";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { StyleSheet, View, type ViewProps } from "react-native";
|
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);
|
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 = ({
|
export const Skeleton = ({
|
||||||
children,
|
|
||||||
show: forcedShow,
|
|
||||||
lines = 1,
|
lines = 1,
|
||||||
variant = "text",
|
variant = "text",
|
||||||
...props
|
...props
|
||||||
}: Omit<ViewProps, "children"> & {
|
}: Omit<ViewProps, "children"> & {
|
||||||
children?: JSX.Element | JSX.Element[] | boolean | null;
|
|
||||||
show?: boolean;
|
|
||||||
lines?: number;
|
lines?: number;
|
||||||
variant?: "text" | "header" | "round" | "custom" | "fill" | "filltext";
|
variant?: "text" | "header" | "round" | "custom" | "fill" | "filltext";
|
||||||
}) => {
|
}) => {
|
||||||
@ -75,8 +35,6 @@ export const Skeleton = ({
|
|||||||
mult.value = withRepeat(withDelay(800, withTiming(1, { duration: 800 })), 0);
|
mult.value = withRepeat(withDelay(800, withTiming(1, { duration: 800 })), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (forcedShow === undefined && children && children !== true) return <>{children}</>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
{...css(
|
{...css(
|
||||||
@ -115,12 +73,11 @@ export const Skeleton = ({
|
|||||||
props,
|
props,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{(forcedShow || !children || children === true) &&
|
{[...Array(lines)].map((_, i) => (
|
||||||
[...Array(lines)].map((_, i) => (
|
|
||||||
<View
|
<View
|
||||||
key={`skeleton_${i}`}
|
key={`skeleton_${i}`}
|
||||||
onLayout={(e) => {
|
onLayout={(e) => {
|
||||||
width.value = e.nativeEvent.layout.width;
|
if (i === 0) width.value = e.nativeEvent.layout.width;
|
||||||
}}
|
}}
|
||||||
{...css([
|
{...css([
|
||||||
{
|
{
|
||||||
@ -154,7 +111,6 @@ export const Skeleton = ({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
{children}
|
|
||||||
</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 })[];
|
accounts: (Account & { select: () => void; remove: () => void })[];
|
||||||
}>({ apiUrl: "api", authToken: null, selectedAccount: null, accounts: [] });
|
}>({ apiUrl: "api", authToken: null, selectedAccount: null, accounts: [] });
|
||||||
|
|
||||||
|
export const useToken = () => {
|
||||||
|
const { apiUrl, authToken } = useContext(AccountContext);
|
||||||
|
return { apiUrl, authToken };
|
||||||
|
};
|
||||||
|
|
||||||
export const useAccount = () => {
|
export const useAccount = () => {
|
||||||
const { selectedAccount } = useContext(AccountContext);
|
const { selectedAccount } = useContext(AccountContext);
|
||||||
return selectedAccount;
|
return selectedAccount;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { type ReactNode, useEffect, useMemo, useRef } from "react";
|
import { type ReactNode, useEffect, useMemo, useRef } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { AccountP, UserP } from "~/models";
|
import { AccountP, UserP } from "~/models";
|
||||||
import { useFetch } from "~/query";
|
import { useFetch } from "~/query";
|
||||||
import { AccountContext } from "./account-context";
|
import { AccountContext } from "./account-context";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { type Account, AccountP } from "~/models";
|
import { type Account, AccountP } from "~/models";
|
||||||
import { readValue, setCookie, storeValue } from "./settings";
|
import { readValue, setCookie, storeValue } from "./settings";
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { MMKV, useMMKVString } from "react-native-mmkv";
|
import { MMKV, useMMKVString } from "react-native-mmkv";
|
||||||
import type { ZodTypeAny, z } from "zod";
|
import type { z, ZodType } from "zod/v4";
|
||||||
import { getServerData } from "~/utils";
|
import { getServerData } from "~/utils";
|
||||||
|
|
||||||
export const storage = new MMKV();
|
export const storage = new MMKV();
|
||||||
@ -24,7 +24,7 @@ export const setCookie = (key: string, val?: unknown) => {
|
|||||||
document.cookie = `${key}=${value};${expires};path=/;samesite=strict`;
|
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");
|
const cookies = getServerData("cookies");
|
||||||
console.log("cookies", cookies);
|
console.log("cookies", cookies);
|
||||||
const decodedCookie = decodeURIComponent(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>;
|
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") {
|
if (Platform.OS === "web" && typeof window === "undefined") {
|
||||||
return readCookie(key, parser);
|
return readCookie(key, parser);
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ export const storeValue = (key: string, value: unknown) => {
|
|||||||
storage.set(key, JSON.stringify(value));
|
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") {
|
if (Platform.OS === "web" && typeof window === "undefined") {
|
||||||
return readCookie(key, parser);
|
return readCookie(key, parser);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
QueryClient,
|
|
||||||
dehydrate,
|
dehydrate,
|
||||||
|
QueryClient,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
@ -8,12 +8,12 @@ import {
|
|||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { Platform } from "react-native";
|
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 { type KyooError, type Page, Paged } from "~/models";
|
||||||
import { AccountContext } from "~/providers/account-context";
|
import { AccountContext } from "~/providers/account-context";
|
||||||
import { setServerData } from "~/utils";
|
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) => {
|
const cleanSlash = (str: string | null, keepFirst = false) => {
|
||||||
if (!str) return null;
|
if (!str) return null;
|
||||||
@ -71,9 +71,9 @@ const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
|||||||
throw data as KyooError;
|
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>;
|
let data: Record<string, any>;
|
||||||
try {
|
try {
|
||||||
@ -82,7 +82,7 @@ const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
|||||||
console.error("Invalid json from kyoo", e);
|
console.error("Invalid json from kyoo", e);
|
||||||
throw { message: "Invalid response from kyoo", status: "json" } as KyooError;
|
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);
|
const parsed = await context.parser.safeParseAsync(data);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
console.log("Url: ", context.url, " Response: ", resp.status, " Parse error: ", parsed.error);
|
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> = {
|
export type QueryIdentifier<T = unknown> = {
|
||||||
parser: z.ZodType<T, z.ZodTypeDef, any>;
|
parser: z.ZodType<T>;
|
||||||
path: (string | undefined)[];
|
path: (string | undefined)[];
|
||||||
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
||||||
infinite?: boolean | { value: true; map?: (x: any[]) => Ret[] };
|
infinite?: boolean;
|
||||||
|
|
||||||
placeholderData?: T | (() => T);
|
placeholderData?: T | (() => T);
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -143,7 +143,8 @@ export const keyToUrl = (key: ReturnType<typeof toQueryKey>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
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 });
|
const key = toQueryKey({ apiUrl, path: query.path, params: query.params });
|
||||||
|
|
||||||
return useQuery<Data, KyooError>({
|
return useQuery<Data, KyooError>({
|
||||||
@ -155,17 +156,18 @@ export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
|||||||
signal: ctx.signal,
|
signal: ctx.signal,
|
||||||
authToken: authToken ?? null,
|
authToken: authToken ?? null,
|
||||||
...query.options,
|
...query.options,
|
||||||
}),
|
}) as Promise<Data>,
|
||||||
placeholderData: query.placeholderData as any,
|
placeholderData: query.placeholderData as any,
|
||||||
enabled: query.enabled,
|
enabled: query.enabled,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
export const useInfiniteFetch = <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 });
|
const key = toQueryKey({ apiUrl, path: query.path, params: query.params });
|
||||||
|
|
||||||
const ret = useInfiniteQuery<Page<Data>, KyooError>({
|
const res = useInfiniteQuery<Page<Data>, KyooError>({
|
||||||
queryKey: key,
|
queryKey: key,
|
||||||
queryFn: (ctx) =>
|
queryFn: (ctx) =>
|
||||||
queryFn({
|
queryFn({
|
||||||
@ -174,20 +176,15 @@ export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) =
|
|||||||
signal: ctx.signal,
|
signal: ctx.signal,
|
||||||
authToken: authToken ?? null,
|
authToken: authToken ?? null,
|
||||||
...query.options,
|
...query.options,
|
||||||
}),
|
}) as Promise<Page<Data>>,
|
||||||
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
|
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
|
||||||
initialPageParam: undefined,
|
initialPageParam: undefined,
|
||||||
placeholderData: query.placeholderData as any,
|
placeholderData: query.placeholderData as any,
|
||||||
enabled: query.enabled,
|
enabled: query.enabled,
|
||||||
});
|
});
|
||||||
const items = ret.data?.pages.flatMap((x) => x.items);
|
const ret = res as typeof res & { items?: Data[] };
|
||||||
return {
|
ret.items = ret.data?.pages.flatMap((x) => x.items);
|
||||||
...ret,
|
return ret;
|
||||||
items:
|
|
||||||
items && typeof query.infinite === "object" && query.infinite.map
|
|
||||||
? query.infinite.map(items)
|
|
||||||
: (items as unknown as Ret[] | undefined),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prefetch = async (...queries: QueryIdentifier[]) => {
|
export const prefetch = async (...queries: QueryIdentifier[]) => {
|
||||||
@ -242,7 +239,7 @@ type MutationParams = {
|
|||||||
body?: object;
|
body?: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMutation = <T = void,>({
|
export const useMutation = <T = void>({
|
||||||
compute,
|
compute,
|
||||||
invalidate,
|
invalidate,
|
||||||
...queryParams
|
...queryParams
|
||||||
|
Loading…
x
Reference in New Issue
Block a user