import { Ionicons } from "@expo/vector-icons"; import { FlashList } from "@shopify/flash-list"; import { type QueryObserverResult, type RefetchOptions, useQuery, } from "@tanstack/react-query"; import { Image } from "expo-image"; import { t } from "i18next"; import { orderBy } from "lodash"; import type React from "react"; import { useCallback, useMemo, useState } from "react"; import { Alert, TouchableOpacity, View } from "react-native"; import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; import { Text } from "@/components/common/Text"; import { Tags } from "@/components/GenreTags"; import { dateOpts } from "@/components/jellyseerr/DetailFacts"; import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; import { RoundButton } from "@/components/RoundButton"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { MediaStatus, MediaType, } from "@/utils/jellyseerr/server/constants/media"; import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import type Season from "@/utils/jellyseerr/server/entity/Season"; import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import { Loader } from "../Loader"; const JellyseerrSeasonEpisodes: React.FC<{ details: TvDetails; seasonNumber: number; }> = ({ details, seasonNumber }) => { const { jellyseerrApi } = useJellyseerr(); const { data: seasonWithEpisodes, isLoading } = useQuery({ queryKey: ["jellyseerr", details.id, "season", seasonNumber], queryFn: async () => jellyseerrApi?.tvSeason(details.id, seasonNumber), enabled: details.seasons.filter((s) => s.seasonNumber !== 0).length > 0, }); return ( item.id} renderItem={(item, index) => ( )} /> ); }; const RenderItem = ({ item }: any) => { const { jellyseerrApi, jellyseerrRegion: region, jellyseerrLocale: locale, } = useJellyseerr(); const [imageError, setImageError] = useState(false); const upcomingAirDate = useMemo(() => { const airDate = item.airDate; if (airDate) { const airDateObj = new Date(airDate); if (new Date() < airDateObj) { return airDateObj.toLocaleDateString(`${locale}-${region}`, dateOpts); } } }, [item, locale, region]); return ( {!imageError ? ( <> { setImageError(true); }} /> {upcomingAirDate && ( {upcomingAirDate} )} ) : ( )} {item.name} {`S${item.seasonNumber}:E${item.episodeNumber}`} {item.overview} ); }; const JellyseerrSeasons: React.FC<{ isLoading: boolean; details?: TvDetails; hasAdvancedRequest?: boolean; onAdvancedRequest?: (data: MediaRequestBody) => void; refetch: ( options?: RefetchOptions | undefined, ) => Promise< QueryObserverResult >; }> = ({ isLoading, details, refetch, hasAdvancedRequest, onAdvancedRequest, }) => { const { jellyseerrApi, requestMedia } = useJellyseerr(); const [seasonStates, setSeasonStates] = useState<{ [key: number]: boolean }>( {}, ); const seasons = useMemo(() => { if (!details) return []; const mediaInfoSeasons = details.mediaInfo?.seasons?.filter( (s: Season) => s.seasonNumber !== 0, ); const requestedSeasons = details.mediaInfo?.requests?.flatMap((r: MediaRequest) => r.seasons) ?? []; return ( details.seasons?.map((season) => ({ ...season, status: mediaInfoSeasons?.find( (mediaSeason: Season) => mediaSeason.seasonNumber === season.seasonNumber, )?.status ?? requestedSeasons?.find( (s: Season) => s.seasonNumber === season.seasonNumber, )?.status ?? MediaStatus.UNKNOWN, })) ?? [] ); }, [details]); const allSeasonsAvailable = useMemo( () => seasons.every((season) => season.status === MediaStatus.AVAILABLE), [seasons], ); const requestAll = useCallback(() => { if (details && jellyseerrApi) { const body: MediaRequestBody = { mediaId: details.id, mediaType: MediaType.TV, tvdbId: details.externalIds?.tvdbId, seasons: seasons .filter( (s) => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0, ) .map((s) => s.seasonNumber), }; if (hasAdvancedRequest) { return onAdvancedRequest?.(body); } requestMedia(details.name, body, refetch); } }, [ jellyseerrApi, seasons, details, hasAdvancedRequest, onAdvancedRequest, requestMedia, refetch, ]); const promptRequestAll = useCallback( () => Alert.alert( t("jellyseerr.confirm"), t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), [ { text: t("jellyseerr.cancel"), style: "cancel", }, { text: t("jellyseerr.yes"), onPress: requestAll, }, ], ), [requestAll], ); const requestSeason = useCallback( async (canRequest: boolean, seasonNumber: number) => { if (canRequest && details) { const body: MediaRequestBody = { mediaId: details.id, mediaType: MediaType.TV, tvdbId: details.externalIds?.tvdbId, seasons: [seasonNumber], }; if (hasAdvancedRequest) { return onAdvancedRequest?.(body); } requestMedia(`${details.name}, Season ${seasonNumber}`, body, refetch); } }, [requestMedia, hasAdvancedRequest, onAdvancedRequest, refetch, details], ); if (!details) return null; if (isLoading) return ( {t("item_card.seasons")} {!allSeasonsAvailable && ( )} ); return ( s.seasonNumber !== 0), "seasonNumber", "desc", )} ListHeaderComponent={() => ( {t("item_card.seasons")} {!allSeasonsAvailable && ( )} )} ItemSeparatorComponent={() => } estimatedItemSize={250} renderItem={({ item: season }) => ( <> setSeasonStates((prevState) => ({ ...prevState, [season.seasonNumber]: !prevState?.[season.seasonNumber], })) } className='px-4' > {[0].map(() => { const canRequest = season.status === MediaStatus.UNKNOWN; return ( requestSeason(canRequest, season.seasonNumber) } className={canRequest ? "bg-gray-700/40" : undefined} mediaStatus={season.status} showRequestIcon={canRequest} /> ); })} {seasonStates?.[season.seasonNumber] && ( )} )} /> ); }; export default JellyseerrSeasons;