diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx index cecb8671..ae8c7b50 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx @@ -1,99 +1,319 @@ -import { useLocalSearchParams } from "expo-router"; -import type React from "react"; -import { useEffect } from "react"; +import type { + BaseItemDto, + MediaSourceInfo, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { Image } from "expo-image"; +import { useNavigation } from "expo-router"; +import { useAtom } from "jotai"; +import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { View } from "react-native"; -import Animated, { - runOnJS, - useAnimatedStyle, - useSharedValue, - withTiming, -} from "react-native-reanimated"; -import { Text } from "@/components/common/Text"; -import { ItemContent } from "@/components/ItemContent"; -import { useItemQuery } from "@/hooks/useItemQuery"; +import { Platform, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { type Bitrate } from "@/components/BitrateSelector"; +import { ItemImage } from "@/components/common/ItemImage"; +import { DownloadSingleItem } from "@/components/DownloadItem"; +import { OverviewText } from "@/components/OverviewText"; +import { ParallaxScrollView } from "@/components/ParallaxPage"; +// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null; +import { PlayButton } from "@/components/PlayButton"; +import { PlayedStatus } from "@/components/PlayedStatus"; +import { SimilarItems } from "@/components/SimilarItems"; +import { CastAndCrew } from "@/components/series/CastAndCrew"; +import { CurrentSeries } from "@/components/series/CurrentSeries"; +import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel"; +import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings"; +import { useImageColorsReturn } from "@/hooks/useImageColorsReturn"; +import { useOrientation } from "@/hooks/useOrientation"; +import * as ScreenOrientation from "@/packages/expo-screen-orientation"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; +import { AddToFavorites } from "./AddToFavorites"; +import { BitrateSheet } from "./BitRateSheet"; +import { ItemHeader } from "./ItemHeader"; +import { ItemTechnicalDetails } from "./ItemTechnicalDetails"; +import { MediaSourceSheet } from "./MediaSourceSheet"; +import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; +import { PlayInRemoteSessionButton } from "./PlayInRemoteSession"; +import { TrackSheet } from "./TrackSheet"; -const Page: React.FC = () => { - const { id } = useLocalSearchParams() as { id: string }; - const { t } = useTranslation(); +const Chromecast = !Platform.isTV ? require("./Chromecast") : null; - const { offline } = useLocalSearchParams() as { offline?: string }; - const isOffline = offline === "true"; - - const { data: item, isError } = useItemQuery(itemId, false, undefined, [ItemFields.MediaSources]); - const { data: mediaSourcesitem, isError } = useItemQuery(id, isOffline); - - const opacity = useSharedValue(1); - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: opacity.value, - }; - }); - - const fadeOut = (callback: any) => { - setTimeout(() => { - opacity.value = withTiming(0, { duration: 500 }, (finished) => { - if (finished) { - runOnJS(callback)(); - } - }); - }, 100); - }; - - const fadeIn = (callback: any) => { - setTimeout(() => { - opacity.value = withTiming(1, { duration: 500 }, (finished) => { - if (finished) { - runOnJS(callback)(); - } - }); - }, 100); - }; - - useEffect(() => { - if (item) { - fadeOut(() => {}); - } else { - fadeIn(() => {}); - } - }, [item]); - - if (isError) - return ( - - {t("item_card.could_not_load_item")} - - ); - - return ( - - - - - - - - - - - - - - - - - {item && } - - ); +export type SelectedOptions = { + bitrate: Bitrate; + mediaSource: MediaSourceInfo | undefined; + audioIndex: number | undefined; + subtitleIndex: number; }; -export default Page; +interface ItemContentProps { + item: BaseItemDto; + isOffline: boolean; + mediaSourcesitem: BaseItemDto; +} + +export const ItemContent: React.FC = React.memo( + ({ item, isOffline, mediaSourcesitem }) => { + const [api] = useAtom(apiAtom); + const { settings } = useSettings(); + const { orientation } = useOrientation(); + const navigation = useNavigation(); + const insets = useSafeAreaInsets(); + const [user] = useAtom(userAtom); + const { t } = useTranslation(); + + const itemColors = useImageColorsReturn({ item }); + + const [loadingLogo, setLoadingLogo] = useState(true); + const [headerHeight, setHeaderHeight] = useState(350); + + const [selectedOptions, setSelectedOptions] = useState< + SelectedOptions | undefined + >(undefined); + + const { + defaultAudioIndex, + defaultBitrate, + defaultMediaSource, + defaultSubtitleIndex, + } = useDefaultPlaySettings(item!, settings); + + const logoUrl = useMemo( + () => (item ? getLogoImageUrlById({ api, item }) : null), + [api, item], + ); + + const loading = useMemo(() => { + return Boolean(logoUrl && loadingLogo); + }, [loadingLogo, logoUrl]); + + // Needs to automatically change the selected to the default values for default indexes. + useEffect(() => { + setSelectedOptions(() => ({ + bitrate: defaultBitrate, + mediaSource: defaultMediaSource, + subtitleIndex: defaultSubtitleIndex ?? -1, + audioIndex: defaultAudioIndex, + })); + }, [ + defaultAudioIndex, + defaultBitrate, + defaultSubtitleIndex, + defaultMediaSource, + ]); + + useEffect(() => { + if (!Platform.isTV) { + navigation.setOptions({ + headerRight: () => + item && + (Platform.OS === "ios" ? ( + + + {item.Type !== "Program" && ( + + {!Platform.isTV && ( + + )} + {user?.Policy?.IsAdministrator && ( + + )} + + + + + )} + + ) : ( + + + {item.Type !== "Program" && ( + + {!Platform.isTV && ( + + )} + {user?.Policy?.IsAdministrator && ( + + )} + + + + + )} + + )), + }); + } + }, [item, navigation, user]); + + useEffect(() => { + if (item) { + if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP) + setHeaderHeight(230); + else if (item.Type === "Movie") setHeaderHeight(500); + else setHeaderHeight(350); + } + }, [item, orientation]); + + if (!item || !selectedOptions) return null; + + return ( + + + + + } + logo={ + logoUrl ? ( + setLoadingLogo(false)} + onError={() => setLoadingLogo(false)} + /> + ) : ( + + ) + } + > + + + + {item.Type !== "Program" && !Platform.isTV && !isOffline && ( + + + setSelectedOptions( + (prev) => prev && { ...prev, bitrate: val }, + ) + } + selected={selectedOptions.bitrate} + /> + + setSelectedOptions( + (prev) => + prev && { + ...prev, + mediaSource: val, + }, + ) + } + selected={selectedOptions.mediaSource} + /> + { + setSelectedOptions( + (prev) => + prev && { + ...prev, + audioIndex: val, + }, + ); + }} + selected={selectedOptions.audioIndex} + /> + + setSelectedOptions( + (prev) => + prev && { + ...prev, + subtitleIndex: val, + }, + ) + } + selected={selectedOptions.subtitleIndex} + /> + + )} + + + + + {item.Type === "Episode" && ( + + )} + + {!isOffline && ( + + )} + + + {item.Type !== "Program" && ( + <> + {item.Type === "Episode" && !isOffline && ( + + )} + + {!isOffline && ( + + )} + + {item.People && item.People.length > 0 && !isOffline && ( + + {item.People.slice(0, 3).map((person, idx) => ( + + ))} + + )} + + {!isOffline && } + + )} + + + + ); + }, +);