mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-16 08:08:18 +00:00
Fixes missing dependencies in useMemo and useCallback hooks to prevent stale closures and potential bugs. Adds null/undefined guards before navigation in music components to prevent crashes when attempting to navigate with missing IDs. Corrects query key from "company" to "genre" in genre page to ensure proper cache invalidation. Updates Jellyseerr references to Seerr throughout documentation and error messages for consistency. Improves type safety by adding error rejection handling in SeerrApi and memoizing components to optimize re-renders.
206 lines
6.9 KiB
TypeScript
206 lines
6.9 KiB
TypeScript
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";
|
|
import { TouchableSeerrRouter } from "@/components/common/SeerrItemRouter";
|
|
import { Text } from "@/components/common/Text";
|
|
import { Tag, Tags } from "@/components/GenreTags";
|
|
import { textShadowStyle } from "@/components/seerr/discover/GenericSlideCard";
|
|
import SeerrMediaIcon from "@/components/seerr/SeerrMediaIcon";
|
|
import SeerrStatusIcon from "@/components/seerr/SeerrStatusIcon";
|
|
import { Colors } from "@/constants/Colors";
|
|
import { useSeerr } from "@/hooks/useSeerr";
|
|
import { useSeerrCanRequest } from "@/utils/_seerr/useSeerrCanRequest";
|
|
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 { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
|
import type {
|
|
MovieResult,
|
|
TvResult,
|
|
} from "@/utils/jellyseerr/server/models/Search";
|
|
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
|
|
|
interface Props extends ViewProps {
|
|
item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast;
|
|
horizontal?: boolean;
|
|
showDownloadInfo?: boolean;
|
|
mediaRequest?: MediaRequest;
|
|
}
|
|
|
|
const SeerrPoster: React.FC<Props> = ({
|
|
item,
|
|
horizontal,
|
|
showDownloadInfo,
|
|
mediaRequest,
|
|
}) => {
|
|
const { seerrApi, getTitle, getYear, getMediaType } = useSeerr();
|
|
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(
|
|
() =>
|
|
seerrApi?.imageProxy(item?.backdropPath, "w1920_and_h800_multi_faces"),
|
|
[item, seerrApi, horizontal],
|
|
);
|
|
|
|
const posterSrc = useMemo(
|
|
() => seerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"),
|
|
[item, seerrApi, 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] = useSeerrCanRequest(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.seerr.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 (
|
|
<TouchableSeerrRouter
|
|
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) > 0 && (
|
|
<Tags
|
|
className='absolute bottom-1 left-0.5 w-32'
|
|
tagProps={{
|
|
className: "bg-black rounded-full px-1",
|
|
}}
|
|
tags={requestedSeasons}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
<SeerrStatusIcon
|
|
className='absolute bottom-1 right-1'
|
|
showRequestIcon={canRequest}
|
|
mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
|
|
/>
|
|
<SeerrMediaIcon
|
|
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>
|
|
</TouchableSeerrRouter>
|
|
);
|
|
};
|
|
|
|
export default SeerrPoster;
|