mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 02:58:28 +01:00
Merge branch 'develop' into feat/i18n
This commit is contained in:
@@ -10,7 +10,7 @@ const CastSlide: React.FC<
|
||||
{ details?: MovieDetails | TvDetails } & ViewProps
|
||||
> = ({ details, ...props }) => {
|
||||
return (
|
||||
details?.credits?.cast?.length &&
|
||||
details?.credits?.cast &&
|
||||
details?.credits?.cast?.length > 0 && (
|
||||
<View {...props}>
|
||||
<Text className="text-lg font-bold mb-2 px-4">Cast</Text>
|
||||
|
||||
@@ -29,8 +29,8 @@ const Facts: React.FC<
|
||||
> = ({ title, facts, ...props }) =>
|
||||
facts &&
|
||||
facts?.length > 0 && (
|
||||
<View className="flex flex-row justify-between py-2" {...props}>
|
||||
<Text className="font-bold">{title}</Text>
|
||||
<View className="flex flex-col justify-between py-2" {...props}>
|
||||
<Text className="font-bold text-start">{title}</Text>
|
||||
|
||||
<View className="flex flex-col items-end">
|
||||
{facts.map((f, idx) =>
|
||||
|
||||
233
components/jellyseerr/RequestModal.tsx
Normal file
233
components/jellyseerr/RequestModal.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, {forwardRef, useCallback, useMemo, useState} from "react";
|
||||
import {View, ViewProps} from "react-native";
|
||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
||||
import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet";
|
||||
import Dropdown from "@/components/common/Dropdown";
|
||||
import {QualityProfile, RootFolder, Tag} from "@/utils/jellyseerr/server/api/servarr/base";
|
||||
import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
|
||||
import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types";
|
||||
import {Button} from "@/components/Button";
|
||||
import {Text} from "@/components/common/Text";
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
title: string,
|
||||
type: MediaType;
|
||||
isAnime?: boolean;
|
||||
is4k?: boolean;
|
||||
onRequested?: () => void;
|
||||
}
|
||||
|
||||
const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps, 'id'>>(({
|
||||
id,
|
||||
title,
|
||||
type,
|
||||
isAnime = false,
|
||||
onRequested,
|
||||
...props
|
||||
}, ref) => {
|
||||
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr();
|
||||
const [requestOverrides, setRequestOverrides] =
|
||||
useState<MediaRequestBody>({
|
||||
mediaId: Number(id),
|
||||
mediaType: type,
|
||||
userId: jellyseerrUser?.id
|
||||
});
|
||||
|
||||
const [modalRequestProps, setModalRequestProps] = useState<MediaRequestBody>();
|
||||
|
||||
const {data: serviceSettings} = useQuery({
|
||||
queryKey: ["jellyseerr", "request", type, 'service'],
|
||||
queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'),
|
||||
enabled: !!jellyseerrApi && !!jellyseerrUser,
|
||||
refetchOnMount: 'always'
|
||||
});
|
||||
|
||||
const {data: users} = useQuery({
|
||||
queryKey: ["jellyseerr", "users"],
|
||||
queryFn: async () => jellyseerrApi?.user({take: 1000, sort: 'displayname'}),
|
||||
enabled: !!jellyseerrApi && !!jellyseerrUser,
|
||||
refetchOnMount: 'always'
|
||||
});
|
||||
|
||||
const defaultService = useMemo(
|
||||
() => serviceSettings?.find?.(v => v.isDefault),
|
||||
[serviceSettings]
|
||||
);
|
||||
|
||||
const {data: defaultServiceDetails} = useQuery({
|
||||
queryKey: ["jellyseerr", "request", type, 'service', 'details', defaultService?.id],
|
||||
queryFn: async () => {
|
||||
setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
serverId: defaultService?.id
|
||||
}))
|
||||
return jellyseerrApi?.serviceDetails(type === 'movie' ? 'radarr' : 'sonarr', defaultService!!.id)
|
||||
},
|
||||
enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService,
|
||||
refetchOnMount: 'always',
|
||||
});
|
||||
|
||||
const defaultProfile: QualityProfile = useMemo(
|
||||
() => defaultServiceDetails?.profiles
|
||||
.find(p =>
|
||||
p.id === (isAnime ? defaultServiceDetails.server?.activeAnimeProfileId : defaultServiceDetails.server?.activeProfileId)
|
||||
),
|
||||
[defaultServiceDetails]
|
||||
);
|
||||
|
||||
const defaultFolder: RootFolder = useMemo(
|
||||
() => defaultServiceDetails?.rootFolders
|
||||
.find(f =>
|
||||
f.path === (isAnime ? defaultServiceDetails?.server.activeAnimeDirectory : defaultServiceDetails.server?.activeDirectory)
|
||||
),
|
||||
[defaultServiceDetails]
|
||||
);
|
||||
|
||||
const defaultTags: Tag[] = useMemo(
|
||||
() => {
|
||||
const tags = defaultServiceDetails?.tags
|
||||
.filter(t =>
|
||||
(isAnime
|
||||
? defaultServiceDetails?.server.activeAnimeTags
|
||||
: defaultServiceDetails?.server.activeTags
|
||||
)?.includes(t.id)
|
||||
) ?? []
|
||||
|
||||
console.log(tags)
|
||||
return tags
|
||||
},
|
||||
[defaultServiceDetails]
|
||||
);
|
||||
|
||||
const seasonTitle = useMemo(
|
||||
() => modalRequestProps?.seasons?.length ? `Season (${modalRequestProps?.seasons})` : undefined,
|
||||
[modalRequestProps?.seasons]
|
||||
);
|
||||
|
||||
const request = useCallback(() => {requestMedia(
|
||||
seasonTitle ? `${title}, ${seasonTitle}` : title,
|
||||
{
|
||||
is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k,
|
||||
profileId: defaultProfile.id,
|
||||
rootFolder: defaultFolder.path,
|
||||
tags: defaultTags.map(t => t.id),
|
||||
...modalRequestProps,
|
||||
...requestOverrides
|
||||
},
|
||||
onRequested
|
||||
)
|
||||
}, [requestOverrides, defaultProfile, defaultFolder, defaultTags]);
|
||||
|
||||
const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`;
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={ref}
|
||||
enableDynamicSizing
|
||||
enableDismissOnClose
|
||||
onDismiss={() => setModalRequestProps(undefined)}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
backdropComponent={(sheetProps: BottomSheetBackdropProps) =>
|
||||
<BottomSheetBackdrop
|
||||
{...sheetProps}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(data) => {
|
||||
setModalRequestProps(data?.data as MediaRequestBody)
|
||||
return <BottomSheetView>
|
||||
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||
<View>
|
||||
<Text className="font-bold text-2xl text-neutral-100">Advanced</Text>
|
||||
{seasonTitle &&
|
||||
<Text className="text-neutral-300">{seasonTitle}</Text>
|
||||
}
|
||||
</View>
|
||||
<View className="flex flex-col space-y-2">
|
||||
{(defaultService && defaultServiceDetails && users) && (
|
||||
<>
|
||||
<Dropdown
|
||||
data={defaultServiceDetails.profiles}
|
||||
titleExtractor={(item) => item.name}
|
||||
placeholderText={defaultProfile.name}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={"Quality Profile"}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
profileId: item?.id
|
||||
}))
|
||||
}
|
||||
title={"Quality Profile"}
|
||||
/>
|
||||
<Dropdown
|
||||
data={defaultServiceDetails.rootFolders}
|
||||
titleExtractor={pathTitleExtractor}
|
||||
placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={"Root Folder"}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
rootFolder: item.path
|
||||
}))}
|
||||
title={"Root Folder"}
|
||||
/>
|
||||
<Dropdown
|
||||
multi={true}
|
||||
data={defaultServiceDetails.tags}
|
||||
titleExtractor={(item) => item.label}
|
||||
placeholderText={defaultTags.map(t => t.label).join(",")}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
label={"Tags"}
|
||||
onSelected={(...item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
tags: item.map(i => i.id)
|
||||
}))
|
||||
}
|
||||
title={"Tags"}
|
||||
/>
|
||||
<Dropdown
|
||||
data={users}
|
||||
titleExtractor={(item) => item.displayName}
|
||||
placeholderText={jellyseerrUser!!.displayName}
|
||||
keyExtractor={(item) => item.id.toString() || ""}
|
||||
label={"Request As"}
|
||||
onSelected={(item) =>
|
||||
item && setRequestOverrides((prev) => ({
|
||||
...prev,
|
||||
userId: item?.id
|
||||
}))
|
||||
}
|
||||
title={"Request As"}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
<Button
|
||||
className="mt-auto"
|
||||
onPress={request}
|
||||
color="purple"
|
||||
>
|
||||
Request
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheetView>
|
||||
}}
|
||||
</BottomSheetModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default RequestModal;
|
||||
@@ -1,23 +1,30 @@
|
||||
import React, {useCallback} from "react";
|
||||
import {
|
||||
useJellyseerr,
|
||||
} from "@/hooks/useJellyseerr";
|
||||
import {TouchableOpacity, ViewProps} from "react-native";
|
||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||
import {COMPANY_LOGO_IMAGE_FILTER, Network} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
|
||||
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
||||
import {Studio} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
|
||||
import {router, useSegments} from "expo-router";
|
||||
import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import {
|
||||
COMPANY_LOGO_IMAGE_FILTER,
|
||||
Network,
|
||||
} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
|
||||
import { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
|
||||
import { router, useSegments } from "expo-router";
|
||||
import React, { useCallback } from "react";
|
||||
import { TouchableOpacity, ViewProps } from "react-native";
|
||||
|
||||
const CompanySlide: React.FC<{data: Network[] | Studio[]} & SlideProps & ViewProps> = ({ slide, data, ...props }) => {
|
||||
const CompanySlide: React.FC<
|
||||
{ data: Network[] | Studio[] } & SlideProps & ViewProps
|
||||
> = ({ slide, data, ...props }) => {
|
||||
const segments = useSegments();
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const from = segments[2];
|
||||
|
||||
const navigate = useCallback(({id, image, name}: Network | Studio) => router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
|
||||
params: {id, image, name, type: slide.type }
|
||||
}), [slide]);
|
||||
const navigate = useCallback(
|
||||
({ id, image, name }: Network | Studio) =>
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
|
||||
params: { id, image, name, type: slide.type },
|
||||
}),
|
||||
[slide]
|
||||
);
|
||||
|
||||
return (
|
||||
<Slide
|
||||
@@ -30,7 +37,10 @@ const CompanySlide: React.FC<{data: Network[] | Studio[]} & SlideProps & ViewPro
|
||||
<GenericSlideCard
|
||||
className="w-28 rounded-lg overflow-hidden border border-neutral-900 p-4"
|
||||
id={item.id.toString()}
|
||||
url={jellyseerrApi?.imageProxy(item.image, COMPANY_LOGO_IMAGE_FILTER)}
|
||||
url={jellyseerrApi?.imageProxy(
|
||||
item.image,
|
||||
COMPANY_LOGO_IMAGE_FILTER
|
||||
)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
@@ -1,55 +1,66 @@
|
||||
import React, {useCallback} from "react";
|
||||
import {Endpoints, useJellyseerr,} from "@/hooks/useJellyseerr";
|
||||
import {TouchableOpacity, ViewProps} from "react-native";
|
||||
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
|
||||
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
||||
import {router, useSegments} from "expo-router";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
|
||||
import {genreColorMap} from "@/utils/jellyseerr/src/components/Discover/constants";
|
||||
import {GenreSliderItem} from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
|
||||
import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide";
|
||||
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
||||
import { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
|
||||
import { genreColorMap } from "@/utils/jellyseerr/src/components/Discover/constants";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { router, useSegments } from "expo-router";
|
||||
import React, { useCallback } from "react";
|
||||
import { TouchableOpacity, ViewProps } from "react-native";
|
||||
|
||||
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
const segments = useSegments();
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const from = segments[2];
|
||||
|
||||
const navigate = useCallback((genre: GenreSliderItem) => router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
|
||||
params: {type: slide.type, name: genre.name}
|
||||
}), [slide]);
|
||||
const navigate = useCallback(
|
||||
(genre: GenreSliderItem) =>
|
||||
router.push({
|
||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
|
||||
params: { type: slide.type, name: genre.name },
|
||||
}),
|
||||
[slide]
|
||||
);
|
||||
|
||||
const {data, isFetching, isLoading } = useQuery({
|
||||
queryKey: ['jellyseerr', 'discover', slide.type, slide.id],
|
||||
const { data, isFetching, isLoading } = useQuery({
|
||||
queryKey: ["jellyseerr", "discover", slide.type, slide.id],
|
||||
queryFn: async () => {
|
||||
return jellyseerrApi?.getGenreSliders(
|
||||
slide.type == DiscoverSliderType.MOVIE_GENRES
|
||||
? Endpoints.MOVIE
|
||||
: Endpoints.TV
|
||||
)
|
||||
);
|
||||
},
|
||||
enabled: !!jellyseerrApi
|
||||
})
|
||||
enabled: !!jellyseerrApi,
|
||||
});
|
||||
|
||||
return (
|
||||
data && <Slide
|
||||
{...props}
|
||||
slide={slide}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={(item, index) => (
|
||||
<TouchableOpacity className="mr-2" onPress={() => navigate(item)}>
|
||||
<GenericSlideCard
|
||||
className="w-28 rounded-lg overflow-hidden border border-neutral-900"
|
||||
id={item.id.toString()}
|
||||
title={item.name}
|
||||
colors={[]}
|
||||
contentFit={"cover"}
|
||||
url={jellyseerrApi?.imageProxy(item.backdrops?.[0], `w780_filter(duotone,${genreColorMap[item.id] ?? genreColorMap[0]})`)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
data && (
|
||||
<Slide
|
||||
{...props}
|
||||
slide={slide}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={(item, index) => (
|
||||
<TouchableOpacity className="mr-2" onPress={() => navigate(item)}>
|
||||
<GenericSlideCard
|
||||
className="w-28 rounded-lg overflow-hidden border border-neutral-900"
|
||||
id={item.id.toString()}
|
||||
title={item.name}
|
||||
colors={[]}
|
||||
contentFit={"cover"}
|
||||
url={jellyseerrApi?.imageProxy(
|
||||
item.backdrops?.[0],
|
||||
`w780_filter(duotone,${
|
||||
genreColorMap[item.id] ?? genreColorMap[0]
|
||||
})`
|
||||
)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user