mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
209 lines
7.0 KiB
TypeScript
209 lines
7.0 KiB
TypeScript
import { Tag, Tags } from "@/components/GenreTags";
|
|
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter";
|
|
import { Text } from "@/components/common/Text";
|
|
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
|
|
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
|
|
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
|
|
import { Colors } from "@/constants/Colors";
|
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
|
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
|
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
|
|
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
|
import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker";
|
|
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
|
import type {
|
|
MovieResult,
|
|
TvResult,
|
|
} from "@/utils/jellyseerr/server/models/Search";
|
|
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
|
import { Image } from "expo-image";
|
|
import { useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { View, type ViewProps } from "react-native";
|
|
import Animated, {
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withTiming,
|
|
} from "react-native-reanimated";
|
|
|
|
interface Props extends ViewProps {
|
|
item?: MovieResult | TvResult | MovieDetails | TvDetails;
|
|
horizontal?: boolean;
|
|
showDownloadInfo?: boolean;
|
|
mediaRequest?: MediaRequest;
|
|
}
|
|
|
|
const JellyseerrPoster: React.FC<Props> = ({
|
|
item,
|
|
horizontal,
|
|
showDownloadInfo,
|
|
mediaRequest,
|
|
...props
|
|
}) => {
|
|
const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr();
|
|
const loadingOpacity = useSharedValue(1);
|
|
const imageOpacity = useSharedValue(0);
|
|
const { t } = useTranslation();
|
|
|
|
const imageAnimatedStyle = useAnimatedStyle(() => ({
|
|
opacity: imageOpacity.value,
|
|
}));
|
|
|
|
const handleImageLoad = () => {
|
|
loadingOpacity.value = withTiming(0, { duration: 200 });
|
|
imageOpacity.value = withTiming(1, { duration: 300 });
|
|
};
|
|
|
|
const backdropSrc = useMemo(
|
|
() =>
|
|
jellyseerrApi?.imageProxy(
|
|
item?.backdropPath,
|
|
"w1920_and_h800_multi_faces",
|
|
),
|
|
[item, jellyseerrApi, horizontal],
|
|
);
|
|
|
|
const posterSrc = useMemo(
|
|
() => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"),
|
|
[item, jellyseerrApi, horizontal],
|
|
);
|
|
|
|
const title = useMemo(() => getTitle(item), [item]);
|
|
const releaseYear = useMemo(() => getYear(item), [item]);
|
|
const mediaType = useMemo(() => getMediaType(item), [item]);
|
|
|
|
const size = useMemo(() => (horizontal ? "h-28" : "w-28"), [horizontal]);
|
|
const ratio = useMemo(() => (horizontal ? "15/10" : "10/15"), [horizontal]);
|
|
|
|
const [canRequest] = useJellyseerrCanRequest(item);
|
|
|
|
const is4k = useMemo(() => mediaRequest?.is4k === true, [mediaRequest]);
|
|
|
|
const downloadItems = useMemo(
|
|
() =>
|
|
(is4k
|
|
? mediaRequest?.media.downloadStatus4k
|
|
: mediaRequest?.media.downloadStatus) || [],
|
|
[mediaRequest, is4k],
|
|
);
|
|
|
|
const progress = useMemo(() => {
|
|
const [totalSize, sizeLeft] = downloadItems.reduce(
|
|
(sum: number[], next: DownloadingItem) => [
|
|
sum[0] + next.size,
|
|
sum[1] + next.sizeLeft,
|
|
],
|
|
[0, 0],
|
|
);
|
|
|
|
return ((totalSize - sizeLeft) / totalSize) * 100;
|
|
}, [downloadItems]);
|
|
|
|
const requestedSeasons: string[] | undefined = useMemo(() => {
|
|
const seasons =
|
|
mediaRequest?.seasons?.flatMap((s) => s.seasonNumber.toString()) || [];
|
|
if (seasons.length > 4) {
|
|
const [first, second, third, fourth, ...rest] = seasons;
|
|
return [
|
|
first,
|
|
second,
|
|
third,
|
|
fourth,
|
|
t("home.settings.plugins.jellyseerr.plus_n_more", { n: rest.length }),
|
|
];
|
|
}
|
|
return seasons;
|
|
}, [mediaRequest]);
|
|
|
|
const available = useMemo(() => {
|
|
const status = mediaRequest?.media?.[is4k ? "status4k" : "status"];
|
|
return status === MediaStatus.AVAILABLE;
|
|
}, [mediaRequest, is4k]);
|
|
|
|
return (
|
|
<TouchableJellyseerrRouter
|
|
result={item}
|
|
mediaTitle={title}
|
|
releaseYear={releaseYear}
|
|
canRequest={canRequest}
|
|
posterSrc={posterSrc!}
|
|
mediaType={mediaType}
|
|
>
|
|
<View className={"flex flex-col mr-2 h-auto"}>
|
|
<View
|
|
className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`}
|
|
>
|
|
<Animated.View style={imageAnimatedStyle}>
|
|
<Image
|
|
className='w-full'
|
|
key={item?.id}
|
|
id={item?.id.toString()}
|
|
source={{ uri: horizontal ? backdropSrc : posterSrc }}
|
|
cachePolicy={"memory-disk"}
|
|
contentFit='cover'
|
|
style={{
|
|
aspectRatio: ratio,
|
|
[horizontal ? "height" : "width"]: "100%",
|
|
}}
|
|
onLoad={handleImageLoad}
|
|
/>
|
|
</Animated.View>
|
|
{mediaRequest && showDownloadInfo && (
|
|
<>
|
|
<View
|
|
className={`absolute w-full h-full bg-black ${!available ? "opacity-70" : "opacity-0"}`}
|
|
/>
|
|
{!available && !Number.isNaN(progress) && (
|
|
<>
|
|
<View
|
|
className='absolute left-0 h-full opacity-40'
|
|
style={{
|
|
width: `${progress || 0}%`,
|
|
backgroundColor: Colors.primaryRGB,
|
|
}}
|
|
/>
|
|
<View className='absolute w-full h-full justify-center items-center'>
|
|
<Text className='font-bold' style={textShadowStyle.shadow}>
|
|
{progress?.toFixed(0)}%
|
|
</Text>
|
|
</View>
|
|
</>
|
|
)}
|
|
<Tag
|
|
className='absolute right-1 top-1 text-right bg-black border border-neutral-800/50'
|
|
text={mediaRequest?.requestedBy.displayName}
|
|
/>
|
|
{requestedSeasons.length > 0 && (
|
|
<Tags
|
|
className='absolute bottom-1 left-0.5 w-32'
|
|
tagProps={{
|
|
className: "bg-black rounded-full px-1",
|
|
}}
|
|
tags={requestedSeasons}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
<JellyseerrStatusIcon
|
|
className='absolute bottom-1 right-1'
|
|
showRequestIcon={canRequest}
|
|
mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
|
|
/>
|
|
<JellyseerrMediaIcon
|
|
className='absolute top-1 left-1'
|
|
mediaType={mediaType}
|
|
/>
|
|
</View>
|
|
</View>
|
|
<View className={`mt-2 flex flex-col ${horizontal ? "w-44" : "w-28"}`}>
|
|
<Text numberOfLines={2}>{title || ""}</Text>
|
|
<Text className='text-xs opacity-50 align-bottom'>
|
|
{releaseYear || ""}
|
|
</Text>
|
|
</View>
|
|
</TouchableJellyseerrRouter>
|
|
);
|
|
};
|
|
|
|
export default JellyseerrPoster;
|