mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Use react-native fast images and blurhash on mobile
This commit is contained in:
parent
c06afcd56d
commit
1faf234255
@ -43,6 +43,7 @@
|
||||
"react-i18next": "^13.0.3",
|
||||
"react-native": "0.72.3",
|
||||
"react-native-blurhash": "^1.1.11",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-mmkv": "^2.10.1",
|
||||
"react-native-reanimated": "~3.3.0",
|
||||
"react-native-safe-area-context": "4.6.3",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-blurhash": "*",
|
||||
"react-native-fast-image": "*",
|
||||
"react-native-reanimated": "*",
|
||||
"react-native-svg": "*",
|
||||
"yoshiki": "*"
|
||||
@ -35,6 +36,9 @@
|
||||
"react-native-blurhash": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-fast-image": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-web": {
|
||||
"optional": true
|
||||
}
|
||||
@ -45,6 +49,8 @@
|
||||
"solito": "^4.0.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"blurhash": "^2.0.5"
|
||||
"blurhash": "^2.0.5",
|
||||
"react-native-blurhash": "^1.1.11",
|
||||
"react-native-fast-image": "^8.6.3"
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export const Avatar = forwardRef<
|
||||
fill?: boolean;
|
||||
as?: ComponentType<{ style?: ViewStyle } & RefAttributes<View>>;
|
||||
} & Stylable
|
||||
>(function _Avatar(
|
||||
>(function Avatar(
|
||||
{ src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, as, ...props },
|
||||
ref,
|
||||
) {
|
||||
|
@ -19,10 +19,8 @@
|
||||
*/
|
||||
|
||||
import { KyooImage } from "@kyoo/models";
|
||||
import {
|
||||
Image as Img,
|
||||
ImageStyle,
|
||||
} from "react-native";
|
||||
import { ReactElement } from "react";
|
||||
import { ImageStyle } from "react-native";
|
||||
import { YoshikiStyle } from "yoshiki/src/type";
|
||||
|
||||
export type YoshikiEnhanced<Style> = Style extends any
|
||||
@ -31,12 +29,13 @@ export type YoshikiEnhanced<Style> = Style extends any
|
||||
}
|
||||
: never;
|
||||
|
||||
export type WithLoading<T> = (T & { isLoading?: boolean }) | (Partial<T> & { isLoading: true });
|
||||
type WithLoading<T> = (T & { isLoading?: false }) | (Partial<T> & { isLoading: true });
|
||||
|
||||
export type Props = WithLoading<{
|
||||
src?: KyooImage | null;
|
||||
alt: string;
|
||||
quality: "low" | "medium" | "high";
|
||||
alt: string;
|
||||
Error?: ReactElement | null;
|
||||
}>;
|
||||
|
||||
export type ImageLayout = YoshikiEnhanced<
|
||||
|
@ -19,9 +19,11 @@
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { ImageProps, ImageStyle, Platform, View, ViewStyle } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { YoshikiEnhanced, WithLoading, Props, ImageLayout } from "./base-image";
|
||||
import { FlexStyle, ImageStyle, View, ViewStyle } from "react-native";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { Blurhash } from "react-native-blurhash";
|
||||
import { percent, useYoshiki } from "yoshiki/native";
|
||||
import { Props, ImageLayout } from "./base-image";
|
||||
import { Skeleton } from "../skeleton";
|
||||
|
||||
export const Image = ({
|
||||
@ -30,6 +32,7 @@ export const Image = ({
|
||||
alt,
|
||||
isLoading: forcedLoading = false,
|
||||
layout,
|
||||
Error,
|
||||
...props
|
||||
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
||||
const { css } = useYoshiki();
|
||||
@ -45,41 +48,37 @@ export const Image = ({
|
||||
setOldSource(src);
|
||||
}
|
||||
|
||||
const border = { borderRadius: 6 } satisfies ViewStyle;
|
||||
const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle;
|
||||
|
||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||
if (!src || state === "errored")
|
||||
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />;
|
||||
|
||||
const nativeProps = Platform.select<Partial<ImageProps>>({
|
||||
web: {
|
||||
defaultSource: typeof src === "string" ? { uri: src } : Array.isArray(src) ? src[0] : src,
|
||||
},
|
||||
default: {},
|
||||
});
|
||||
if (!src || state === "errored") {
|
||||
return Error !== undefined ? (
|
||||
Error
|
||||
) : (
|
||||
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View {...css(layout)}>
|
||||
<Blurhash src={src.high} blurhash={src.blurhash} />
|
||||
<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 ?? "high"] }}
|
||||
accessibilityLabel={alt}
|
||||
onLoad={() => setState("finished")}
|
||||
onError={() => setState("errored")}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
{...(css({
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
}) as { style: FlexStyle })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Skeleton variant="custom" show={state === "loading"} {...css([layout, border], props)}>
|
||||
// <Img
|
||||
// source={{ uri: src[quality || "high"] }}
|
||||
// accessibilityLabel={alt}
|
||||
// onLoad={() => setState("finished")}
|
||||
// onError={() => setState("errored")}
|
||||
// {...nativeProps}
|
||||
// {...css([
|
||||
// {
|
||||
// width: percent(100),
|
||||
// height: percent(100),
|
||||
// resizeMode: "cover",
|
||||
// },
|
||||
// ])}
|
||||
// />
|
||||
// </Skeleton>
|
||||
// );
|
||||
};
|
||||
|
@ -21,6 +21,7 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { ImageStyle, View, ViewStyle } from "react-native";
|
||||
import { StyleList, processStyleList } from "yoshiki/src/type";
|
||||
import { useYoshiki as useWebYoshiki } from "yoshiki/web";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Props, ImageLayout } from "./base-image";
|
||||
import { blurHashToDataURL } from "./blurhash-web";
|
||||
@ -41,9 +42,11 @@ export const Image = ({
|
||||
alt,
|
||||
isLoading: forcedLoading = false,
|
||||
layout,
|
||||
Error,
|
||||
...props
|
||||
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
||||
const { css } = useYoshiki();
|
||||
const { css: wCss } = useWebYoshiki();
|
||||
const [state, setState] = useState<"loading" | "errored" | "finished">(
|
||||
src ? "finished" : "errored",
|
||||
);
|
||||
@ -55,8 +58,13 @@ export const Image = ({
|
||||
const border = { borderRadius: 6 } satisfies ViewStyle;
|
||||
|
||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||
if (!src || state === "errored")
|
||||
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />;
|
||||
if (!src || state === "errored") {
|
||||
return Error !== undefined ? (
|
||||
Error
|
||||
) : (
|
||||
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
||||
);
|
||||
}
|
||||
|
||||
const blurhash = blurHashToDataURL(src.blurhash);
|
||||
return (
|
||||
@ -79,10 +87,9 @@ export const Image = ({
|
||||
width: (layout as any).width,
|
||||
height: (layout as any).height,
|
||||
aspectRatio: (layout as any).aspectRatio,
|
||||
...border,
|
||||
}}
|
||||
// Gather classnames from props (to support parent's hover for example).
|
||||
className={extractClassNames(props)}
|
||||
{...wCss({ ...border, borderRadius: "6px" }, { className: extractClassNames(props) })}
|
||||
>
|
||||
<NextImage
|
||||
src={src[quality ?? "high"]}
|
||||
|
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { ImageProps, ImageStyle, Platform, View, ViewProps, ViewStyle } from "react-native";
|
||||
import { Props, ImageLayout, YoshikiEnhanced } from "./base-image";
|
||||
import { Props, YoshikiEnhanced } from "./base-image";
|
||||
import { Image } from "./image";
|
||||
import { ComponentType, ReactNode, useState } from "react";
|
||||
import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
|
||||
@ -36,12 +36,18 @@ export const Poster = ({
|
||||
}: Props & { style?: ImageStyle } & {
|
||||
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
|
||||
}) => (
|
||||
<Image isLoading={isLoading} alt={alt} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />
|
||||
<Image
|
||||
isLoading={isLoading as any}
|
||||
alt={alt!}
|
||||
layout={{ aspectRatio: 2 / 3, ...layout }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ImageBackground = <AsProps = ViewProps,>({
|
||||
src,
|
||||
alt,
|
||||
quality,
|
||||
gradient = true,
|
||||
as,
|
||||
children,
|
||||
@ -57,14 +63,6 @@ export const ImageBackground = <AsProps = ViewProps,>({
|
||||
imageStyle?: YoshikiEnhanced<ImageStyle>;
|
||||
} & AsProps &
|
||||
Props) => {
|
||||
const [isErrored, setErrored] = useState(false);
|
||||
|
||||
const nativeProps = Platform.select<Partial<ImageProps>>({
|
||||
web: {
|
||||
defaultSource: typeof src === "string" ? { uri: src! } : Array.isArray(src) ? src[0] : src!,
|
||||
},
|
||||
default: {},
|
||||
});
|
||||
const Container = as ?? View;
|
||||
return (
|
||||
<ContrastArea contrastText>
|
||||
@ -84,16 +82,14 @@ export const ImageBackground = <AsProps = ViewProps,>({
|
||||
containerStyle,
|
||||
])}
|
||||
>
|
||||
{src && !isErrored && (
|
||||
{src && (
|
||||
<Image
|
||||
source={typeof src === "string" ? { uri: src } : src}
|
||||
accessibilityLabel={alt}
|
||||
onError={() => setErrored(true)}
|
||||
{...nativeProps}
|
||||
{...css([
|
||||
{ width: percent(100), height: percent(100), resizeMode: "cover" },
|
||||
imageStyle,
|
||||
])}
|
||||
src={src}
|
||||
quality={quality}
|
||||
alt={alt!}
|
||||
layout={{ width: percent(100), height: percent(100) }}
|
||||
Error={null}
|
||||
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as ImageProps)}
|
||||
/>
|
||||
)}
|
||||
{gradient && (
|
||||
|
@ -2526,6 +2526,8 @@ __metadata:
|
||||
"@tanstack/react-query": ^4.32.6
|
||||
"@types/react": 18.2.0
|
||||
blurhash: ^2.0.5
|
||||
react-native-blurhash: ^1.1.11
|
||||
react-native-fast-image: ^8.6.3
|
||||
solito: ^4.0.1
|
||||
typescript: ^5.1.6
|
||||
peerDependencies:
|
||||
@ -2538,12 +2540,17 @@ __metadata:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
react-native-blurhash: "*"
|
||||
react-native-fast-image: "*"
|
||||
react-native-reanimated: "*"
|
||||
react-native-svg: "*"
|
||||
yoshiki: "*"
|
||||
dependenciesMeta:
|
||||
blurhash:
|
||||
optional: true
|
||||
react-native-blurhash:
|
||||
optional: true
|
||||
react-native-fast-image:
|
||||
optional: true
|
||||
peerDependenciesMeta:
|
||||
"@gorhom/portal":
|
||||
optional: true
|
||||
@ -2553,6 +2560,8 @@ __metadata:
|
||||
optional: true
|
||||
react-native-blurhash:
|
||||
optional: true
|
||||
react-native-fast-image:
|
||||
optional: true
|
||||
react-native-web:
|
||||
optional: true
|
||||
languageName: unknown
|
||||
@ -10565,6 +10574,7 @@ __metadata:
|
||||
react-i18next: ^13.0.3
|
||||
react-native: 0.72.3
|
||||
react-native-blurhash: ^1.1.11
|
||||
react-native-fast-image: ^8.6.3
|
||||
react-native-mmkv: ^2.10.1
|
||||
react-native-reanimated: ~3.3.0
|
||||
react-native-safe-area-context: 4.6.3
|
||||
@ -11882,6 +11892,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-fast-image@npm:^8.6.3":
|
||||
version: 8.6.3
|
||||
resolution: "react-native-fast-image@npm:8.6.3"
|
||||
peerDependencies:
|
||||
react: ^17 || ^18
|
||||
react-native: ">=0.60.0"
|
||||
checksum: 29289cb6b2eae0983c8922b22e2d9de3be07322bb7991c5def19f95eadefaedb0e308ff0b38cc1d0444e8bd4fe94a7621a99a2d3d9298100bcb60b3144677234
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-mmkv@npm:^2.10.1":
|
||||
version: 2.10.1
|
||||
resolution: "react-native-mmkv@npm:2.10.1"
|
||||
|
Loading…
x
Reference in New Issue
Block a user