mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
feat: [Jellyseerr] Show recent requests #324
- Added recent requests slide - updated JellyseerrPoster.tsx to handle more options
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
69
components/jellyseerr/discover/RecentRequestsSlide.tsx
Normal file
69
components/jellyseerr/discover/RecentRequestsSlide.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 ",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 ",
|
||||||
|
|||||||
@@ -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 ",
|
||||||
|
|||||||
@@ -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": "マーリン検索を有効にする ",
|
||||||
|
|||||||
@@ -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 ",
|
||||||
|
|||||||
@@ -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 ",
|
||||||
|
|||||||
@@ -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 搜索",
|
||||||
|
|||||||
@@ -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 搜索",
|
||||||
|
|||||||
Reference in New Issue
Block a user