import { Ionicons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { atom, useAtom } from "jotai"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { SeasonDropdown, type SeasonIndexState, } from "@/components/series/SeasonDropdown"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { buildOfflineSeasons, getDownloadedEpisodesForSeason, } from "@/utils/downloads/offline-series"; import { runtimeTicksToSeconds } from "@/utils/time"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { Text } from "../common/Text"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; import { DownloadItems, DownloadSingleItem } from "../DownloadItem"; import { Loader } from "../Loader"; import { PlayedStatus } from "../PlayedStatus"; type Props = { item: BaseItemDto; initialSeasonIndex?: number; }; export const seasonIndexAtom = atom({}); export const SeasonPicker: React.FC = ({ item }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); const { t } = useTranslation(); const isOffline = useOfflineMode(); const { getDownloadedItems, downloadedItems } = useDownload(); const seasonIndex = useMemo( () => seasonIndexState[item.Id ?? ""], [item, seasonIndexState], ); const { data: seasons } = useQuery({ queryKey: ["seasons", item.Id, isOffline, downloadedItems.length], queryFn: async () => { if (isOffline) { return buildOfflineSeasons(getDownloadedItems(), item.Id!); } if (!api || !user?.Id || !item.Id) return []; const response = await api.axiosInstance.get( `${api.basePath}/Shows/${item.Id}/Seasons`, { params: { userId: user?.Id, itemId: item.Id, Fields: "ItemCounts,PrimaryImageAspectRatio,CanDelete,MediaSourceCount", }, headers: { Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, }, }, ); return response.data.Items; }, staleTime: isOffline ? Infinity : 60, enabled: isOffline || (!!api && !!user?.Id && !!item.Id), }); const selectedSeasonId: string | null = useMemo(() => { const season: BaseItemDto = seasons?.find( (s: BaseItemDto) => s.IndexNumber === seasonIndex || s.Name === seasonIndex, ); if (!season?.Id) return null; return season.Id!; }, [seasons, seasonIndex]); // For offline mode, we use season index number instead of ID const selectedSeasonNumber = useMemo(() => { if (!isOffline) return null; const season = seasons?.find( (s: BaseItemDto) => s.IndexNumber === seasonIndex || s.Name === seasonIndex, ); return season?.IndexNumber ?? null; }, [isOffline, seasons, seasonIndex]); const { data: episodes, isPending } = useQuery({ queryKey: [ "episodes", item.Id, isOffline ? selectedSeasonNumber : selectedSeasonId, isOffline, downloadedItems.length, ], queryFn: async () => { if (isOffline) { return getDownloadedEpisodesForSeason( getDownloadedItems(), item.Id!, selectedSeasonNumber!, ); } if (!api || !user?.Id || !item.Id || !selectedSeasonId) { return []; } const res = await getTvShowsApi(api).getEpisodes({ seriesId: item.Id, userId: user.Id, seasonId: selectedSeasonId, enableUserData: true, fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"], }); if (res.data.TotalRecordCount === 0) console.warn( "No episodes found for season with ID ~", selectedSeasonId, ); return res.data.Items; }, staleTime: isOffline ? Infinity : 0, enabled: isOffline ? !!item.Id && selectedSeasonNumber !== null : !!api && !!user?.Id && !!item.Id && !!selectedSeasonId, }); // Used for height calculation const [nrOfEpisodes, setNrOfEpisodes] = useState(0); useEffect(() => { if (episodes && episodes.length > 0) { setNrOfEpisodes(episodes.length); } }, [episodes]); return ( { if (!item.Id) return; setSeasonIndexState((prev) => ({ ...prev, [item.Id!]: season.IndexNumber ?? season.Name, })); }} /> {episodes?.length && !isOffline ? ( ( )} DownloadedIconComponent={() => ( )} /> ) : null} {isPending ? ( ) : ( episodes?.map((e: BaseItemDto) => ( {e.Name} {`S${e.ParentIndexNumber?.toString()}:E${e.IndexNumber?.toString()}`} {runtimeTicksToSeconds(e.RunTimeTicks)} {!isOffline && ( )} {e.Overview} )) )} {(episodes?.length || 0) === 0 ? ( {t("item_card.no_episodes_for_this_season")} ) : null} ); };