import { AudioTrackSelector } from "@/components/AudioTrackSelector"; import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; import { DownloadItem } from "@/components/DownloadItem"; import { OverviewText } from "@/components/OverviewText"; import { ParallaxScrollView } from "@/components/ParallaxPage"; import { PlayButton } from "@/components/PlayButton"; import { PlayedStatus } from "@/components/PlayedStatus"; import { SimilarItems } from "@/components/SimilarItems"; import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector"; import { ItemImage } from "@/components/common/ItemImage"; import { CastAndCrew } from "@/components/series/CastAndCrew"; import { CurrentSeries } from "@/components/series/CurrentSeries"; import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel"; import { useImageColors } from "@/hooks/useImageColors"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getItemImage } from "@/utils/getItemImage"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; import { chromecastProfile } from "@/utils/profiles/chromecast"; import ios from "@/utils/profiles/ios"; import native from "@/utils/profiles/native"; import old from "@/utils/profiles/old"; import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { Stack, useNavigation } from "expo-router"; import * as ScreenOrientation from "expo-screen-orientation"; import { useAtom } from "jotai"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { View } from "react-native"; import { useCastDevice } from "react-native-google-cast"; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Chromecast } from "./Chromecast"; import { ItemHeader } from "./ItemHeader"; import { Loader } from "./Loader"; import { MediaSourceSelector } from "./MediaSourceSelector"; export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const opacity = useSharedValue(0); const castDevice = useCastDevice(); const navigation = useNavigation(); const [settings] = useSettings(); const [selectedMediaSource, setSelectedMediaSource] = useState(null); const [selectedAudioStream, setSelectedAudioStream] = useState(-1); const [selectedSubtitleStream, setSelectedSubtitleStream] = useState(-1); const [maxBitrate, setMaxBitrate] = useState({ key: "Max", value: undefined, }); const [loadingLogo, setLoadingLogo] = useState(true); const [orientation, setOrientation] = useState( ScreenOrientation.Orientation.PORTRAIT_UP ); useEffect(() => { const subscription = ScreenOrientation.addOrientationChangeListener( (event) => { setOrientation(event.orientationInfo.orientation); } ); ScreenOrientation.getOrientationAsync().then((initialOrientation) => { setOrientation(initialOrientation); }); return () => { ScreenOrientation.removeOrientationChangeListener(subscription); }; }, []); const animatedStyle = useAnimatedStyle(() => { return { opacity: opacity.value, }; }); const fadeIn = () => { opacity.value = withTiming(1, { duration: 300 }); }; const fadeOut = (callback: any) => { opacity.value = withTiming(0, { duration: 300 }, (finished) => { if (finished) { runOnJS(callback)(); } }); }; const headerHeightRef = useRef(400); const { data: item, isLoading, isFetching, } = useQuery({ queryKey: ["item", id], queryFn: async () => { const res = await getUserItemData({ api, userId: user?.Id, itemId: id, }); console.log("itemID", res?.Id); return res; }, enabled: !!id && !!api, staleTime: 60 * 1000 * 5, }); const [localItem, setLocalItem] = useState(item); useEffect(() => { if (item) { if (localItem) { // Fade out current item fadeOut(() => { // Update local item after fade out setLocalItem(item); // Then fade in fadeIn(); }); } else { // If there's no current item, just set and fade in setLocalItem(item); fadeIn(); } } else { // If item is null, fade out and clear local item fadeOut(() => setLocalItem(null)); } }, [item]); useEffect(() => { navigation.setOptions({ headerRight: () => item && ( ), }); }, [item]); useEffect(() => { if (orientation !== ScreenOrientation.Orientation.PORTRAIT_UP) { headerHeightRef.current = 230; return; } if (item?.Type === "Episode") headerHeightRef.current = 400; else if (item?.Type === "Movie") headerHeightRef.current = 500; else headerHeightRef.current = 400; }, [item]); const { data: sessionData } = useQuery({ queryKey: ["sessionData", item?.Id], queryFn: async () => { if (!api || !user?.Id || !item?.Id) return null; const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({ itemId: item?.Id, userId: user?.Id, }); return playbackData.data; }, enabled: !!item?.Id && !!api && !!user?.Id, staleTime: 0, }); const { data: playbackUrl } = useQuery({ queryKey: [ "playbackUrl", item?.Id, maxBitrate, castDevice, selectedMediaSource, selectedAudioStream, selectedSubtitleStream, settings, ], queryFn: async () => { if (!api || !user?.Id || !sessionData || !selectedMediaSource?.Id) return null; let deviceProfile: any = ios; if (castDevice?.deviceId) { deviceProfile = chromecastProfile; } else if (settings?.deviceProfile === "Native") { deviceProfile = native; } else if (settings?.deviceProfile === "Old") { deviceProfile = old; } const url = await getStreamUrl({ api, userId: user.Id, item, startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0, maxStreamingBitrate: maxBitrate.value, sessionData, deviceProfile, audioStreamIndex: selectedAudioStream, subtitleStreamIndex: selectedSubtitleStream, forceDirectPlay: settings?.forceDirectPlay, height: maxBitrate.height, mediaSourceId: selectedMediaSource.Id, }); console.info("Stream URL:", url); return url; }, enabled: !!sessionData && !!api && !!user?.Id && !!item?.Id, staleTime: 0, }); const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]); const themeImageColorSource = useMemo(() => { if (!api || !item) return; return getItemImage({ item, api, variant: "Primary", quality: 80, width: 300, }); }, [api, item]); useImageColors(themeImageColorSource?.uri); const loading = useMemo(() => { return Boolean(isLoading || isFetching || (logoUrl && loadingLogo)); }, [isLoading, isFetching, loadingLogo, logoUrl]); const insets = useSafeAreaInsets(); return ( {loading && ( )} {localItem && ( )} } logo={ <> {logoUrl ? ( setLoadingLogo(false)} onError={() => setLoadingLogo(false)} /> ) : null} } > {localItem ? ( setMaxBitrate(val)} selected={maxBitrate} /> {selectedMediaSource && ( <> )} ) : ( )} {item?.Type === "Episode" && ( )} {item?.Type === "Episode" && ( )} ); });