import { Ionicons } from "@expo/vector-icons"; import { BottomSheetBackdrop, type BottomSheetBackdropProps, BottomSheetModal, BottomSheetTextInput, BottomSheetView, } from "@gorhom/bottom-sheet"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation, useRouter } from "expo-router"; import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Platform, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { toast } from "sonner-native"; import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; import { GenreTags } from "@/components/GenreTags"; import Cast from "@/components/jellyseerr/Cast"; import DetailFacts from "@/components/jellyseerr/DetailFacts"; import RequestModal from "@/components/jellyseerr/RequestModal"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; import { PlatformDropdown } from "@/components/PlatformDropdown"; import { JellyserrRatings } from "@/components/Ratings"; import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; import { ItemActions } from "@/components/series/SeriesActions"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { type IssueType, IssueTypeName, } from "@/utils/jellyseerr/server/constants/issue"; import { MediaRequestStatus, MediaType, } from "@/utils/jellyseerr/server/constants/media"; import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; import { hasPermission, Permission, } from "@/utils/jellyseerr/server/lib/permissions"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieResult, TvResult, } from "@/utils/jellyseerr/server/models/Search"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; const Page: React.FC = () => { const insets = useSafeAreaInsets(); const params = useLocalSearchParams(); const { t } = useTranslation(); const router = useRouter(); const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } = params as unknown as { mediaTitle: string; releaseYear: number; canRequest: string; posterSrc: string; mediaType: MediaType; } & Partial; const navigation = useNavigation(); const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); const [issueType, setIssueType] = useState(); const [issueMessage, setIssueMessage] = useState(); const [requestBody, _setRequestBody] = useState(); const [issueTypeDropdownOpen, setIssueTypeDropdownOpen] = useState(false); const advancedReqModalRef = useRef(null); const bottomSheetModalRef = useRef(null); const { data: details, isFetching, isLoading, refetch, } = useQuery({ enabled: !!jellyseerrApi && !!result && !!result.id, queryKey: ["jellyseerr", "detail", mediaType, result.id], staleTime: 0, refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: true, retryOnMount: true, refetchInterval: 0, queryFn: async () => { return mediaType === MediaType.MOVIE ? jellyseerrApi?.movieDetails(result.id!) : jellyseerrApi?.tvDetails(result.id!); }, }); const [canRequest, hasAdvancedRequestPermission] = useJellyseerrCanRequest(details); const canManageRequests = useMemo(() => { if (!jellyseerrUser) return false; return hasPermission( Permission.MANAGE_REQUESTS, jellyseerrUser.permissions, ); }, [jellyseerrUser]); const pendingRequest = useMemo(() => { return details?.mediaInfo?.requests?.find( (r: MediaRequest) => r.status === MediaRequestStatus.PENDING, ); }, [details]); const handleApproveRequest = useCallback(async () => { if (!pendingRequest?.id) return; try { await jellyseerrApi?.approveRequest(pendingRequest.id); toast.success(t("jellyseerr.toasts.request_approved")); refetch(); } catch (error) { toast.error(t("jellyseerr.toasts.failed_to_approve_request")); console.error("Failed to approve request:", error); } }, [jellyseerrApi, pendingRequest, refetch, t]); const handleDeclineRequest = useCallback(async () => { if (!pendingRequest?.id) return; try { await jellyseerrApi?.declineRequest(pendingRequest.id); toast.success(t("jellyseerr.toasts.request_declined")); refetch(); } catch (error) { toast.error(t("jellyseerr.toasts.failed_to_decline_request")); console.error("Failed to decline request:", error); } }, [jellyseerrApi, pendingRequest, refetch, t]); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( ), [], ); const submitIssue = useCallback(() => { if (result.id && issueType && issueMessage && details) { jellyseerrApi ?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage) .then(() => { setIssueType(undefined); setIssueMessage(undefined); bottomSheetModalRef?.current?.close(); }); } }, [jellyseerrApi, details, result, issueType, issueMessage]); const handleIssueModalDismiss = useCallback(() => { setIssueTypeDropdownOpen(false); }, []); const setRequestBody = useCallback( (body: MediaRequestBody) => { _setRequestBody(body); advancedReqModalRef?.current?.present?.(); }, [requestBody, _setRequestBody, advancedReqModalRef], ); const request = useCallback(async () => { const body: MediaRequestBody = { mediaId: Number(result.id!), mediaType: mediaType!, tvdbId: details?.externalIds?.tvdbId, ...(mediaType === MediaType.TV && { seasons: (details as TvDetails)?.seasons ?.filter?.((s) => s.seasonNumber !== 0) ?.map?.((s) => s.seasonNumber), }), }; if (hasAdvancedRequestPermission) { setRequestBody(body); return; } requestMedia(mediaTitle, body, refetch); }, [ details, result, requestMedia, hasAdvancedRequestPermission, mediaTitle, refetch, mediaType, ]); const isAnime = useMemo( () => (details?.keywords.some((k) => k.id === ANIME_KEYWORD_ID) || false) && mediaType === MediaType.TV, [details], ); const issueTypeOptionGroups = useMemo( () => [ { title: t("jellyseerr.types"), options: Object.entries(IssueTypeName) .reverse() .map(([key, value]) => ({ type: "radio" as const, label: value, value: key, selected: key === String(issueType), onPress: () => setIssueType(key as unknown as IssueType), })), }, ], [issueType, t], ); useEffect(() => { if (details) { navigation.setOptions({ headerRight: () => ( ), }); } }, [details]); return ( {result.backdropPath ? ( ) : ( )} } > {mediaTitle} {releaseYear} g.name) || []} /> {isLoading || isFetching ? ( ) : ( details?.mediaInfo?.jellyfinMediaId && ( {!Platform.isTV && ( )} ) )} {canManageRequests && pendingRequest && ( {t("jellyseerr.requested_by", { user: pendingRequest.requestedBy?.displayName || pendingRequest.requestedBy?.username || pendingRequest.requestedBy?.jellyfinUsername || t("jellyseerr.unknown_user"), })} )} {mediaType === MediaType.TV && ( setRequestBody(data)} /> )} { _setRequestBody(undefined); advancedReqModalRef?.current?.close(); refetch(); }} onDismiss={() => _setRequestBody(undefined)} /> {!Platform.isTV && ( // This is till it's fixed because the menu isn't selectable on TV {t("jellyseerr.whats_wrong")} {t("jellyseerr.issue_type")} {issueType ? IssueTypeName[issueType] : t("jellyseerr.select_an_issue")} } title={t("jellyseerr.types")} open={issueTypeDropdownOpen} onOpenChange={setIssueTypeDropdownOpen} /> )} ); }; export default Page;