mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 02:27:11 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			202 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			202 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /*
 | |
|  * 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 { ComponentType, ReactNode, useState } from "react";
 | |
| import {
 | |
| 	View,
 | |
| 	Image as Img,
 | |
| 	ImageSourcePropType,
 | |
| 	ImageStyle,
 | |
| 	Platform,
 | |
| 	ImageProps,
 | |
| 	ViewProps,
 | |
| 	ViewStyle,
 | |
| } from "react-native";
 | |
| import { percent, useYoshiki } from "yoshiki/native";
 | |
| import { YoshikiStyle } from "yoshiki/dist/type";
 | |
| import { Skeleton } from "./skeleton";
 | |
| import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
 | |
| import { alpha, ContrastArea } from "./themes";
 | |
| 
 | |
| type YoshikiEnhanced<Style> = Style extends any
 | |
| 	? {
 | |
| 			[key in keyof Style]: YoshikiStyle<Style[key]>;
 | |
| 	  }
 | |
| 	: never;
 | |
| 
 | |
| type WithLoading<T> = (T & { isLoading?: boolean }) | (Partial<T> & { isLoading: true });
 | |
| 
 | |
| type Props = WithLoading<{
 | |
| 	src?: string | ImageSourcePropType | null;
 | |
| 	alt?: string;
 | |
| }>;
 | |
| 
 | |
| type ImageLayout = YoshikiEnhanced<
 | |
| 	| { width: ViewStyle["width"]; height: ViewStyle["height"] }
 | |
| 	| { width: ViewStyle["width"]; aspectRatio: ViewStyle["aspectRatio"] }
 | |
| 	| { height: ViewStyle["height"]; aspectRatio: ViewStyle["aspectRatio"] }
 | |
| >;
 | |
| 
 | |
| export const Image = ({
 | |
| 	src,
 | |
| 	alt,
 | |
| 	isLoading: forcedLoading = false,
 | |
| 	layout,
 | |
| 	...props
 | |
| }: Props & { style?: ViewStyle } & { 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 } 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<ImageProps>({
 | |
| 		web: {
 | |
| 			defaultSource: typeof src === "string" ? { uri: src } : Array.isArray(src) ? src[0] : src,
 | |
| 		},
 | |
| 		default: {},
 | |
| 	});
 | |
| 
 | |
| 	return (
 | |
| 		<Skeleton variant="custom" show={state === "loading"} {...css([layout, border], props)}>
 | |
| 			<Img
 | |
| 				source={typeof src === "string" ? { uri: src } : src}
 | |
| 				accessibilityLabel={alt}
 | |
| 				onLoad={() => setState("finished")}
 | |
| 				onError={() => setState("errored")}
 | |
| 				{...nativeProps}
 | |
| 				{...css([
 | |
| 					{
 | |
| 						width: percent(100),
 | |
| 						height: percent(100),
 | |
| 						resizeMode: "cover",
 | |
| 					},
 | |
| 				])}
 | |
| 			/>
 | |
| 		</Skeleton>
 | |
| 	);
 | |
| };
 | |
| 
 | |
| export const Poster = ({
 | |
| 	alt,
 | |
| 	isLoading = false,
 | |
| 	layout,
 | |
| 	...props
 | |
| }: Props & { style?: ViewStyle } & {
 | |
| 	layout: YoshikiEnhanced<{ width: ViewStyle["width"] } | { height: ViewStyle["height"] }>;
 | |
| }) => (
 | |
| 	<Image isLoading={isLoading} alt={alt} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />
 | |
| );
 | |
| 
 | |
| export const ImageBackground = <AsProps = ViewProps,>({
 | |
| 	src,
 | |
| 	alt,
 | |
| 	gradient = true,
 | |
| 	as,
 | |
| 	children,
 | |
| 	containerStyle,
 | |
| 	imageStyle,
 | |
| 	isLoading,
 | |
| 	...asProps
 | |
| }: {
 | |
| 	as?: ComponentType<AsProps>;
 | |
| 	gradient?: Partial<LinearGradientProps> | boolean;
 | |
| 	children: ReactNode;
 | |
| 	containerStyle?: YoshikiEnhanced<ViewStyle>;
 | |
| 	imageStyle?: YoshikiEnhanced<ImageStyle>;
 | |
| } & AsProps &
 | |
| 	Props) => {
 | |
| 	const [isErrored, setErrored] = useState(false);
 | |
| 
 | |
| 	const nativeProps = Platform.select<ImageProps>({
 | |
| 		web: {
 | |
| 			defaultSource: typeof src === "string" ? { uri: src! } : Array.isArray(src) ? src[0] : src!,
 | |
| 		},
 | |
| 		default: {},
 | |
| 	});
 | |
| 	const Container = as ?? View;
 | |
| 	return (
 | |
| 		<ContrastArea contrastText>
 | |
| 			{({ css, theme }) => (
 | |
| 				<Container {...(asProps as AsProps)}>
 | |
| 					<View
 | |
| 						{...css([
 | |
| 							{
 | |
| 								position: "absolute",
 | |
| 								top: 0,
 | |
| 								bottom: 0,
 | |
| 								left: 0,
 | |
| 								right: 0,
 | |
| 								zIndex: -1,
 | |
| 								bg: (theme) => theme.background,
 | |
| 							},
 | |
| 							containerStyle,
 | |
| 						])}
 | |
| 					>
 | |
| 						{src && !isErrored && (
 | |
| 							<Img
 | |
| 								source={typeof src === "string" ? { uri: src } : src}
 | |
| 								accessibilityLabel={alt}
 | |
| 								onError={() => setErrored(true)}
 | |
| 								{...nativeProps}
 | |
| 								{...css([
 | |
| 									{ width: percent(100), height: percent(100), resizeMode: "cover" },
 | |
| 									imageStyle,
 | |
| 								])}
 | |
| 							/>
 | |
| 						)}
 | |
| 						{gradient && (
 | |
| 							<LinearGradient
 | |
| 								start={{ x: 0, y: 0.25 }}
 | |
| 								end={{ x: 0, y: 1 }}
 | |
| 								colors={["transparent", alpha(theme.colors.black, 0.6)]}
 | |
| 								{...css(
 | |
| 									{
 | |
| 										position: "absolute",
 | |
| 										top: 0,
 | |
| 										bottom: 0,
 | |
| 										left: 0,
 | |
| 										right: 0,
 | |
| 									},
 | |
| 									typeof gradient === "object" ? gradient : undefined,
 | |
| 								)}
 | |
| 							/>
 | |
| 						)}
 | |
| 					</View>
 | |
| 					{children}
 | |
| 				</Container>
 | |
| 			)}
 | |
| 		</ContrastArea>
 | |
| 	);
 | |
| };
 |