Files
streamyfin/components/posters/SeerrPoster.tsx
Uruk 4a75e8f551 refactor: rename Jellyseerr to Seerr throughout codebase
Updates branding and naming conventions to use "Seerr" instead of "Jellyseerr" across all files, components, hooks, and translations.

Renames files, functions, classes, variables, and UI text to reflect the new naming convention while maintaining identical functionality. Updates asset references including logo and screenshot images.

Changes API class name, storage keys, atom names, and all related utilities to use "Seerr" prefix. Modifies translation keys and user-facing text to match the rebrand.
2026-01-12 09:26:19 +01:00

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 && (
<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;