feat: [Jellyseerr] Show recent requests #324

- Added recent requests slide
- updated JellyseerrPoster.tsx to handle more options
This commit is contained in:
herrrta
2025-03-03 01:04:49 -05:00
parent 951158bcd3
commit dd65505f7f
21 changed files with 317 additions and 86 deletions

View File

@@ -42,19 +42,21 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import RequestModal from "@/components/jellyseerr/RequestModal"; import RequestModal from "@/components/jellyseerr/RequestModal";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
const Page: React.FC = () => { const Page: React.FC = () => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const params = useLocalSearchParams(); const params = useLocalSearchParams();
const { t } = useTranslation(); const { t } = useTranslation();
const { mediaTitle, releaseYear, posterSrc, ...result } = const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
params as unknown as { params as unknown as {
mediaTitle: string; mediaTitle: string;
releaseYear: number; releaseYear: number;
canRequest: string; canRequest: string;
posterSrc: string; posterSrc: string;
} & Partial<MovieResult | TvResult>; mediaType: MediaType;
} & Partial<MovieResult | TvResult | MovieDetails | TvDetails>;
const navigation = useNavigation(); const navigation = useNavigation();
const { jellyseerrApi, requestMedia } = useJellyseerr(); const { jellyseerrApi, requestMedia } = useJellyseerr();
@@ -72,7 +74,7 @@ const Page: React.FC = () => {
refetch, refetch,
} = useQuery({ } = useQuery({
enabled: !!jellyseerrApi && !!result && !!result.id, enabled: !!jellyseerrApi && !!result && !!result.id,
queryKey: ["jellyseerr", "detail", result.mediaType, result.id], queryKey: ["jellyseerr", "detail", mediaType, result.id],
staleTime: 0, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
refetchOnReconnect: true, refetchOnReconnect: true,
@@ -80,7 +82,7 @@ const Page: React.FC = () => {
retryOnMount: true, retryOnMount: true,
refetchInterval: 0, refetchInterval: 0,
queryFn: async () => { queryFn: async () => {
return result.mediaType === MediaType.MOVIE return mediaType === MediaType.MOVIE
? jellyseerrApi?.movieDetails(result.id!!) ? jellyseerrApi?.movieDetails(result.id!!)
: jellyseerrApi?.tvDetails(result.id!!); : jellyseerrApi?.tvDetails(result.id!!);
}, },
@@ -120,7 +122,7 @@ const Page: React.FC = () => {
const request = useCallback(async () => { const request = useCallback(async () => {
const body: MediaRequestBody = { const body: MediaRequestBody = {
mediaId: Number(result.id!!), mediaId: Number(result.id!!),
mediaType: result.mediaType!!, mediaType: mediaType!!,
tvdbId: details?.externalIds?.tvdbId, tvdbId: details?.externalIds?.tvdbId,
seasons: (details as TvDetails)?.seasons seasons: (details as TvDetails)?.seasons
?.filter?.((s) => s.seasonNumber !== 0) ?.filter?.((s) => s.seasonNumber !== 0)
@@ -138,7 +140,7 @@ const Page: React.FC = () => {
const isAnime = useMemo( const isAnime = useMemo(
() => () =>
(details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) && (details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) &&
result.mediaType === MediaType.TV, mediaType === MediaType.TV,
[details] [details]
); );
@@ -206,7 +208,7 @@ const Page: React.FC = () => {
<View className="px-4"> <View className="px-4">
<View className="flex flex-row justify-between w-full"> <View className="flex flex-row justify-between w-full">
<View className="flex flex-col w-56"> <View className="flex flex-col w-56">
<JellyserrRatings result={result as MovieResult | TvResult} /> <JellyserrRatings result={result as MovieResult | TvResult | MovieDetails | TvDetails} />
<Text <Text
uiTextView uiTextView
selectable selectable
@@ -253,10 +255,9 @@ const Page: React.FC = () => {
<OverviewText text={result.overview} className="mt-4" /> <OverviewText text={result.overview} className="mt-4" />
</View> </View>
{result.mediaType === MediaType.TV && ( {mediaType === MediaType.TV && (
<JellyseerrSeasons <JellyseerrSeasons
isLoading={isLoading || isFetching} isLoading={isLoading || isFetching}
result={result as TvResult}
details={details as TvDetails} details={details as TvDetails}
refetch={refetch} refetch={refetch}
hasAdvancedRequest={hasAdvancedRequestPermission} hasAdvancedRequest={hasAdvancedRequestPermission}
@@ -278,7 +279,7 @@ const Page: React.FC = () => {
requestBody={requestBody} requestBody={requestBody}
title={mediaTitle} title={mediaTitle}
id={result.id!!} id={result.id!!}
type={result.mediaType as MediaType} type={mediaType}
isAnime={isAnime} isAnime={isAnime}
onRequested={() => { onRequested={() => {
_setRequestBody(undefined) _setRequestBody(undefined)

View File

@@ -21,14 +21,19 @@ export const Tag: React.FC<{ text: string, textClass?: ViewProps["className"], t
); );
}; };
export const Tags: React.FC<TagProps & ViewProps> = ({ tags, textClass = "text-xs", ...props }) => { export const Tags: React.FC<TagProps & {tagProps?: ViewProps} & ViewProps> = ({
tags,
textClass = "text-xs",
tagProps,
...props
}) => {
if (!tags || tags.length === 0) return null; if (!tags || tags.length === 0) return null;
return ( return (
<View className={`flex flex-row flex-wrap gap-1 ${props.className}`} {...props}> <View className={`flex flex-row flex-wrap gap-1 ${props.className}`} {...props}>
{tags.map((tag, idx) => ( {tags.map((tag, idx) => (
<View key={idx}> <View key={idx}>
<Tag key={idx} textClass={textClass} text={tag}/> <Tag key={idx} textClass={textClass} text={tag} {...tagProps}/>
</View> </View>
))} ))}
</View> </View>

View File

@@ -7,6 +7,9 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
import {useMemo} from "react";
interface Props extends ViewProps { interface Props extends ViewProps {
item?: BaseItemDto | null; item?: BaseItemDto | null;
@@ -49,14 +52,17 @@ export const Ratings: React.FC<Props> = ({ item, ...props }) => {
); );
}; };
export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult }> = ({ export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult | TvDetails | MovieDetails }> = ({
result, result,
}) => { }) => {
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi, getMediaType } = useJellyseerr();
const mediaType = useMemo(() => getMediaType(result), [result]);
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["jellyseerr", result.id, result.mediaType, "ratings"], queryKey: ["jellyseerr", result.id, mediaType, "ratings"],
queryFn: async () => { queryFn: async () => {
return result.mediaType === MediaType.MOVIE return mediaType === MediaType.MOVIE
? jellyseerrApi?.movieRatings(result.id) ? jellyseerrApi?.movieRatings(result.id)
: jellyseerrApi?.tvRatings(result.id); : jellyseerrApi?.tvRatings(result.id);
}, },

View File

@@ -9,13 +9,16 @@ import {
Permission, Permission,
} from "@/utils/jellyseerr/server/lib/permissions"; } from "@/utils/jellyseerr/server/lib/permissions";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
result: MovieResult | TvResult; result: MovieResult | TvResult | MovieDetails | TvDetails;
mediaTitle: string; mediaTitle: string;
releaseYear: number; releaseYear: number;
canRequest: boolean; canRequest: boolean;
posterSrc: string; posterSrc: string;
mediaType: MediaType;
} }
export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
@@ -24,6 +27,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
releaseYear, releaseYear,
canRequest, canRequest,
posterSrc, posterSrc,
mediaType,
children, children,
...props ...props
}) => { }) => {
@@ -46,7 +50,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
() => () =>
requestMedia(mediaTitle, { requestMedia(mediaTitle, {
mediaId: result.id, mediaId: result.id,
mediaType: result.mediaType, mediaType,
}), }),
[jellyseerrApi, result] [jellyseerrApi, result]
); );
@@ -67,6 +71,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
releaseYear, releaseYear,
canRequest, canRequest,
posterSrc, posterSrc,
mediaType
}, },
}); });
}} }}
@@ -83,7 +88,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
key={"content"} key={"content"}
> >
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label> <ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
{canRequest && result.mediaType === MediaType.MOVIE && ( {canRequest && mediaType === MediaType.MOVIE && (
<ContextMenu.Item <ContextMenu.Item
key="item-1" key="item-1"
onSelect={() => { onSelect={() => {

View File

@@ -8,6 +8,7 @@ import {View} from "react-native";
import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider"; import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide"; import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide";
interface Props { interface Props {
sliders?: DiscoverSlider[]; sliders?: DiscoverSlider[];
@@ -25,6 +26,8 @@ const Discover: React.FC<Props> = ({ sliders }) => {
<View className="flex flex-col space-y-4 mb-8"> <View className="flex flex-col space-y-4 mb-8">
{sortedSliders.map(slide => { {sortedSliders.map(slide => {
switch (slide.type) { switch (slide.type) {
case DiscoverSliderType.RECENT_REQUESTS:
return <RecentRequestsSlide key={slide.id} slide={slide} />
case DiscoverSliderType.NETWORKS: case DiscoverSliderType.NETWORKS:
return <CompanySlide key={slide.id} slide={slide} data={networks}/> return <CompanySlide key={slide.id} slide={slide} data={networks}/>
case DiscoverSliderType.STUDIOS: case DiscoverSliderType.STUDIOS:

View File

@@ -0,0 +1,69 @@
import React from "react";
import {useQuery} from "@tanstack/react-query";
import {useJellyseerr} from "@/hooks/useJellyseerr";
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
import {ViewProps} from "react-native";
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import {NonFunctionProperties} from "@/utils/jellyseerr/server/interfaces/api/common";
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => {
const {jellyseerrApi} = useJellyseerr();
const { data: details, isLoading, isError } = useQuery({
queryKey: ["jellyseerr", "detail", request.media.mediaType, request.media.tmdbId],
queryFn: async () => {
return request.media.mediaType == MediaType.MOVIE
? jellyseerrApi?.movieDetails(request.media.tmdbId)
: jellyseerrApi?.tvDetails(request.media.tmdbId);
},
enabled: !!jellyseerrApi,
refetchOnMount: true,
staleTime: 0,
});
const { data: refreshedRequest } = useQuery({
queryKey: ["jellyseerr", "requests", request.media.mediaType, request.id],
queryFn: async () => jellyseerrApi?.getRequest(request.id),
enabled: !!jellyseerrApi,
refetchOnMount: true,
refetchInterval: 5000,
staleTime: 0,
});
return (
details && <JellyseerrPoster horizontal showDownloadInfo item={details} mediaRequest={refreshedRequest} />
)
}
const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const {jellyseerrApi} = useJellyseerr();
const { data: requests, isLoading, isError } = useQuery({
queryKey: ["jellyseerr", "recent_requests"],
queryFn: async () => jellyseerrApi?.requests(),
enabled: !!jellyseerrApi,
refetchOnMount: true,
staleTime: 0,
});
return (
requests &&
requests.results.length > 0 &&
!isError && (
<Slide
{...props}
slide={slide}
data={requests.results}
keyExtractor={(item) => item.id.toString()}
renderItem={(item: NonFunctionProperties<MediaRequest>) => (
<RequestCard request={item}/>
)}
/>
)
)
};
export default RecentRequestsSlide;

View File

@@ -1,28 +1,42 @@
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter"; import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter";
import { Text } from "@/components/common/Text"; import {Text} from "@/components/common/Text";
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import {useJellyseerr} from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; import {useJellyseerrCanRequest} from "@/utils/_jellyseerr/useJellyseerrCanRequest";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import {Image} from "expo-image";
import { Image } from "expo-image"; import {useMemo} from "react";
import { useMemo } from "react"; import {View, ViewProps} from "react-native";
import { View, ViewProps } from "react-native"; import Animated, {useAnimatedStyle, useSharedValue, withTiming,} from "react-native-reanimated";
import Animated, { import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
useAnimatedStyle, import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie";
useSharedValue, import type {DownloadingItem} from "@/utils/jellyseerr/server/lib/downloadtracker";
withTiming, import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
} from "react-native-reanimated"; import {useTranslation} from "react-i18next";
import {MediaStatus} from "@/utils/jellyseerr/server/constants/media";
import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard";
import {Colors} from "@/constants/Colors";
import {Tags} from "@/components/GenreTags";
interface Props extends ViewProps { interface Props extends ViewProps {
item: MovieResult | TvResult; item: MovieResult | TvResult | MovieDetails | TvDetails;
horizontal?: boolean;
showDownloadInfo?: boolean;
mediaRequest?: MediaRequest;
} }
const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => { const JellyseerrPoster: React.FC<Props> = ({
const { jellyseerrApi } = useJellyseerr(); item,
horizontal,
showDownloadInfo,
mediaRequest,
...props
}) => {
const { jellyseerrApi, getTitle, getYear, getMediaType, isJellyseerrResult } = useJellyseerr();
const loadingOpacity = useSharedValue(1); const loadingOpacity = useSharedValue(1);
const imageOpacity = useSharedValue(0); const imageOpacity = useSharedValue(0);
const {t} = useTranslation();
const loadingAnimatedStyle = useAnimatedStyle(() => ({ const loadingAnimatedStyle = useAnimatedStyle(() => ({
opacity: loadingOpacity.value, opacity: loadingOpacity.value,
@@ -38,27 +52,64 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
}; };
const imageSrc = useMemo( const imageSrc = useMemo(
() => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"), () => jellyseerrApi?.imageProxy(
[item, jellyseerrApi] horizontal ? item.backdropPath : item.posterPath,
horizontal ? "w1920_and_h800_multi_faces" : "w300_and_h450_face"
),
[item, jellyseerrApi, horizontal]
); );
const title = useMemo( const title = useMemo(() => getTitle(item), [item]);
() => (item.mediaType === MediaType.MOVIE ? item.title : item.name), const releaseYear = useMemo(() => getYear(item), [item]);
[item] const mediaType = useMemo(() => getMediaType(item), [item]);
);
const releaseYear = useMemo( const size = useMemo(() => horizontal ? 'h-28' : 'w-28', [horizontal])
() => const ratio = useMemo(() => horizontal ? '15/10' : '10/15', [horizontal])
new Date(
item.mediaType === MediaType.MOVIE
? item.releaseDate
: item.firstAirDate
).getFullYear(),
[item]
);
const [canRequest] = useJellyseerrCanRequest(item); 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 ( return (
<TouchableJellyseerrRouter <TouchableJellyseerrRouter
result={item} result={item}
@@ -66,9 +117,10 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
releaseYear={releaseYear} releaseYear={releaseYear}
canRequest={canRequest} canRequest={canRequest}
posterSrc={imageSrc!!} posterSrc={imageSrc!!}
mediaType={mediaType}
> >
<View className="flex flex-col w-28 mr-2"> <View className={`flex flex-col mr-2 h-auto`}>
<View className="relative rounded-lg overflow-hidden border border-neutral-900 w-28 aspect-[10/15]"> <View className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`}>
<Animated.View style={imageAnimatedStyle}> <Animated.View style={imageAnimatedStyle}>
<Image <Image
key={item.id} key={item.id}
@@ -77,26 +129,65 @@ const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
cachePolicy={"memory-disk"} cachePolicy={"memory-disk"}
contentFit="cover" contentFit="cover"
style={{ style={{
aspectRatio: "10/15", aspectRatio: ratio,
width: "100%", [horizontal ? 'height' : 'width']: "100%"
}} }}
onLoad={handleImageLoad} onLoad={handleImageLoad}
/> />
</Animated.View> </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>
</>
)}
<Text
className="absolute right-1 top-1 text-right font-bold"
style={textShadowStyle.shadow}
>
{mediaRequest?.requestedBy.displayName}
</Text>
{requestedSeasons.length > 0 && (
<Tags
className="absolute bottom-1 left-0.5 w-32"
tagProps={{
className: "bg-black rounded-full px-1"
}}
tags={requestedSeasons}
/>
)}
</>
)}
<JellyseerrStatusIcon <JellyseerrStatusIcon
className="absolute bottom-1 right-1" className="absolute bottom-1 right-1"
showRequestIcon={canRequest} showRequestIcon={canRequest}
mediaStatus={item?.mediaInfo?.status} mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
/> />
<JellyseerrMediaIcon <JellyseerrMediaIcon
className="absolute top-1 left-1" className="absolute top-1 left-1"
mediaType={item?.mediaType} mediaType={mediaType}
/> />
</View> </View>
<View className="mt-2 flex flex-col"> </View>
<Text numberOfLines={2}>{title}</Text> <View className={`mt-2 flex flex-col ${horizontal ? 'w-44' : 'w-28'}`}>
<Text className="text-xs opacity-50 align-bottom">{releaseYear}</Text> <Text numberOfLines={2}>{title}</Text>
</View> <Text className="text-xs opacity-50 align-bottom">{releaseYear}</Text>
</View> </View>
</TouchableJellyseerrRouter> </TouchableJellyseerrRouter>
); );

View File

@@ -128,14 +128,12 @@ const RenderItem = ({ item, index }: any) => {
const JellyseerrSeasons: React.FC<{ const JellyseerrSeasons: React.FC<{
isLoading: boolean; isLoading: boolean;
result?: TvResult;
details?: TvDetails; details?: TvDetails;
hasAdvancedRequest?: boolean, hasAdvancedRequest?: boolean,
onAdvancedRequest?: (data: MediaRequestBody) => void; onAdvancedRequest?: (data: MediaRequestBody) => void;
refetch: (options?: (RefetchOptions | undefined)) => Promise<QueryObserverResult<TvDetails | MovieDetails | undefined, Error>>; refetch: (options?: (RefetchOptions | undefined)) => Promise<QueryObserverResult<TvDetails | MovieDetails | undefined, Error>>;
}> = ({ }> = ({
isLoading, isLoading,
result,
details, details,
refetch, refetch,
hasAdvancedRequest, hasAdvancedRequest,
@@ -195,7 +193,7 @@ const JellyseerrSeasons: React.FC<{
return onAdvancedRequest?.(body) return onAdvancedRequest?.(body)
} }
requestMedia(result?.name!!, body, refetch); requestMedia(details.name, body, refetch);
} }
}, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]); }, [jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest]);
@@ -227,7 +225,7 @@ const JellyseerrSeasons: React.FC<{
return onAdvancedRequest?.(body) return onAdvancedRequest?.(body)
} }
requestMedia(`${result?.name!!}, Season ${seasonNumber}`, body, refetch); requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch);
} }
}, [requestMedia, hasAdvancedRequest, onAdvancedRequest]); }, [requestMedia, hasAdvancedRequest, onAdvancedRequest]);

View File

@@ -8,6 +8,7 @@ import { toast } from "sonner-native";
import { ListGroup } from "../list/ListGroup"; import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import {Colors} from "@/constants/Colors";
export const StorageSettings = () => { export const StorageSettings = () => {
const { deleteAllFiles, appSizeUsage } = useDownload(); const { deleteAllFiles, appSizeUsage } = useDownload();
@@ -61,7 +62,7 @@ export const StorageSettings = () => {
<View <View
style={{ style={{
width: `${(size.app / size.total) * 100}%`, width: `${(size.app / size.total) * 100}%`,
backgroundColor: "rgb(147 51 234)", backgroundColor: Colors.primaryRGB,
}} }}
/> />
<View <View
@@ -70,7 +71,7 @@ export const StorageSettings = () => {
((size.total - size.remaining - size.app) / size.total) * ((size.total - size.remaining - size.app) / size.total) *
100 100
}%`, }%`,
backgroundColor: "rgb(192 132 252)", backgroundColor: Colors.primaryLightRGB,
}} }}
/> />
</> </>

View File

@@ -1,5 +1,7 @@
export const Colors = { export const Colors = {
primary: "#9334E9", primary: "#9334E9",
primaryRGB: "rgb(147 51 234)",
primaryLightRGB: "rgb(192 132 252)",
text: "#ECEDEE", text: "#ECEDEE",
background: "#151718", background: "#151718",
tint: "#fff", tint: "#fff",

View File

@@ -1,5 +1,5 @@
import axios, { AxiosError, AxiosInstance } from "axios"; import axios, { AxiosError, AxiosInstance } from "axios";
import { Results } from "@/utils/jellyseerr/server/models/Search"; import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import { inRange } from "lodash"; import { inRange } from "lodash";
import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User";
@@ -14,7 +14,7 @@ import {
MediaType, MediaType,
} from "@/utils/jellyseerr/server/constants/media"; } from "@/utils/jellyseerr/server/constants/media";
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; import {MediaRequestBody, RequestResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { import {
SeasonWithEpisodes, SeasonWithEpisodes,
@@ -227,6 +227,23 @@ export class JellyseerrApi {
.then(({ data }) => data); .then(({ data }) => data);
} }
async getRequest(id: number): Promise<MediaRequest> {
return this.axios
?.get<MediaRequest>(Endpoints.API_V1 + Endpoints.REQUEST + `/${id}`)
.then(({ data }) => data);
}
async requests(params = {
filter: "all",
take: 10,
sort: "modified",
skip: 0
}): Promise<RequestResultsResponse> {
return this.axios
?.get<RequestResultsResponse>(Endpoints.API_V1 + Endpoints.REQUEST, {params})
.then(({data}) => data);
}
async movieDetails(id: number) { async movieDetails(id: number) {
return this.axios return this.axios
?.get<MovieDetails>(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`) ?.get<MovieDetails>(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`)
@@ -439,14 +456,34 @@ export const useJellyseerr = () => {
); );
const isJellyseerrResult = ( const isJellyseerrResult = (
items: any[] | null | undefined items: any | null | undefined
): items is Results[] => { ): items is Results => {
return ( return (
!items || items &&
(items.length >= 0 && Object.hasOwn(items, "mediaType") &&
Object.hasOwn(items[0], "mediaType") && Object.values(MediaType).includes(items["mediaType"])
Object.values(MediaType).includes(items[0]["mediaType"])) )
); };
const getTitle = (item: TvResult | TvDetails | MovieResult | MovieDetails) => {
return isJellyseerrResult(item)
? (item.mediaType == MediaType.MOVIE ? item?.originalTitle : item?.name)
: (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name)
};
const getYear = (item: TvResult | TvDetails | MovieResult | MovieDetails) => {
return new Date((
isJellyseerrResult(item)
? (item.mediaType == MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate)
: (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate))
|| ""
)?.getFullYear?.()
};
const getMediaType = (item: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => {
return isJellyseerrResult(item)
? item.mediaType
: item?.mediaInfo?.mediaType
}; };
const jellyseerrRegion = useMemo( const jellyseerrRegion = useMemo(
@@ -464,6 +501,9 @@ export const useJellyseerr = () => {
setJellyseerrUser, setJellyseerrUser,
clearAllJellyseerData, clearAllJellyseerData,
isJellyseerrResult, isJellyseerrResult,
getTitle,
getYear,
getMediaType,
jellyseerrRegion, jellyseerrRegion,
jellyseerrLocale, jellyseerrLocale,
requestMedia, requestMedia,

View File

@@ -168,7 +168,8 @@
"tv_quota_limit": "TV-Anfragelimit", "tv_quota_limit": "TV-Anfragelimit",
"tv_quota_days": "TV-Anfragetage", "tv_quota_days": "TV-Anfragetage",
"reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück", "reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück",
"unlimited": "Unlimitiert" "unlimited": "Unlimitiert",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Aktiviere Marlin Search", "enable_marlin_search": "Aktiviere Marlin Search",

View File

@@ -168,7 +168,8 @@
"tv_quota_limit": "TV quota limit", "tv_quota_limit": "TV quota limit",
"tv_quota_days": "TV quota days", "tv_quota_days": "TV quota days",
"reset_jellyseerr_config_button": "Reset Jellyseerr config", "reset_jellyseerr_config_button": "Reset Jellyseerr config",
"unlimited": "Unlimited" "unlimited": "Unlimited",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Enable Marlin Search ", "enable_marlin_search": "Enable Marlin Search ",

View File

@@ -168,7 +168,8 @@
"tv_quota_limit": "Límite de cuota de series", "tv_quota_limit": "Límite de cuota de series",
"tv_quota_days": "Días de cuota de series", "tv_quota_days": "Días de cuota de series",
"reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr", "reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr",
"unlimited": "Ilimitado" "unlimited": "Ilimitado",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Habilitar búsqueda de Marlin", "enable_marlin_search": "Habilitar búsqueda de Marlin",

View File

@@ -169,7 +169,8 @@
"tv_quota_limit": "Limite de quota TV", "tv_quota_limit": "Limite de quota TV",
"tv_quota_days": "Jours de quota TV", "tv_quota_days": "Jours de quota TV",
"reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr", "reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr",
"unlimited": "Illimité" "unlimited": "Illimité",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Activer Marlin Search ", "enable_marlin_search": "Activer Marlin Search ",

View File

@@ -168,7 +168,8 @@
"tv_quota_limit": "Limite di quota per le serie TV", "tv_quota_limit": "Limite di quota per le serie TV",
"tv_quota_days": "Giorni di quota per le serie TV", "tv_quota_days": "Giorni di quota per le serie TV",
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr", "reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
"unlimited": "Illimitato" "unlimited": "Illimitato",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Abilita la ricerca Marlin ", "enable_marlin_search": "Abilita la ricerca Marlin ",

View File

@@ -167,7 +167,8 @@
"tv_quota_limit": "テレビのクオータ制限", "tv_quota_limit": "テレビのクオータ制限",
"tv_quota_days": "テレビのクオータ日数", "tv_quota_days": "テレビのクオータ日数",
"reset_jellyseerr_config_button": "Jellyseerrの設定をリセット", "reset_jellyseerr_config_button": "Jellyseerrの設定をリセット",
"unlimited": "無制限" "unlimited": "無制限",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "マーリン検索を有効にする ", "enable_marlin_search": "マーリン検索を有効にする ",

View File

@@ -168,7 +168,8 @@
"tv_quota_limit": "Limiet serie quota", "tv_quota_limit": "Limiet serie quota",
"tv_quota_days": "Serie Quota dagen", "tv_quota_days": "Serie Quota dagen",
"reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen", "reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen",
"unlimited": "Ongelimiteerd" "unlimited": "Ongelimiteerd",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Marlin Search inschakelen ", "enable_marlin_search": "Marlin Search inschakelen ",

View File

@@ -167,7 +167,8 @@
"tv_quota_limit": "TV kota limiti", "tv_quota_limit": "TV kota limiti",
"tv_quota_days": "TV kota günleri", "tv_quota_days": "TV kota günleri",
"reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla", "reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla",
"unlimited": "Sınırsız" "unlimited": "Sınırsız",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "Marlin Aramasını Etkinleştir ", "enable_marlin_search": "Marlin Aramasını Etkinleştir ",

View File

@@ -167,7 +167,8 @@
"tv_quota_limit": "剧集配额限制", "tv_quota_limit": "剧集配额限制",
"tv_quota_days": "剧集配额天数", "tv_quota_days": "剧集配额天数",
"reset_jellyseerr_config_button": "重置 Jellyseerr 设置", "reset_jellyseerr_config_button": "重置 Jellyseerr 设置",
"unlimited": "无限制" "unlimited": "无限制",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "启用 Marlin 搜索", "enable_marlin_search": "启用 Marlin 搜索",

View File

@@ -167,7 +167,8 @@
"tv_quota_limit": "電視配額限制", "tv_quota_limit": "電視配額限制",
"tv_quota_days": "電視配額天數", "tv_quota_days": "電視配額天數",
"reset_jellyseerr_config_button": "重置 Jellyseerr 配置", "reset_jellyseerr_config_button": "重置 Jellyseerr 配置",
"unlimited": "無限制" "unlimited": "無限制",
"plus_n_more": "+{{n}} more"
}, },
"marlin_search": { "marlin_search": {
"enable_marlin_search": "啟用 Marlin 搜索", "enable_marlin_search": "啟用 Marlin 搜索",