mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
289 lines
9.4 KiB
TypeScript
289 lines
9.4 KiB
TypeScript
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 } from "@/providers/JellyfinProvider";
|
|
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
|
import { useSettings } from "@/utils/atoms/settings";
|
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
|
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
|
import {
|
|
BaseItemDto,
|
|
MediaSourceInfo,
|
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
import { Image } from "expo-image";
|
|
import { useFocusEffect, useNavigation } from "expo-router";
|
|
import * as ScreenOrientation from "expo-screen-orientation";
|
|
import { useAtom } from "jotai";
|
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { Alert, View } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { Chromecast } from "./Chromecast";
|
|
import { ItemHeader } from "./ItemHeader";
|
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
|
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
|
|
|
export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|
({ item }) => {
|
|
const [api] = useAtom(apiAtom);
|
|
const { setPlaySettings, playUrl, playSettings } = usePlaySettings();
|
|
const [settings] = useSettings();
|
|
const navigation = useNavigation();
|
|
|
|
const [loadingLogo, setLoadingLogo] = useState(true);
|
|
|
|
const [orientation, setOrientation] = useState(
|
|
ScreenOrientation.Orientation.PORTRAIT_UP
|
|
);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
if (!settings) return;
|
|
const { bitrate, mediaSource, audioIndex, subtitleIndex } =
|
|
getDefaultPlaySettings(item, settings);
|
|
|
|
setPlaySettings({
|
|
item,
|
|
bitrate,
|
|
mediaSource,
|
|
audioIndex,
|
|
subtitleIndex,
|
|
});
|
|
|
|
if (!mediaSource) {
|
|
Alert.alert("Error", "No media source found for this item.");
|
|
navigation.goBack();
|
|
}
|
|
}, [item, settings])
|
|
);
|
|
|
|
const selectedMediaSource = useMemo(() => {
|
|
return playSettings?.mediaSource || undefined;
|
|
}, [playSettings?.mediaSource]);
|
|
|
|
const setSelectedMediaSource = (mediaSource: MediaSourceInfo) => {
|
|
setPlaySettings((prev) => ({
|
|
...prev,
|
|
mediaSource,
|
|
}));
|
|
};
|
|
|
|
const selectedAudioStream = useMemo(() => {
|
|
return playSettings?.audioIndex;
|
|
}, [playSettings?.audioIndex]);
|
|
|
|
const setSelectedAudioStream = (audioIndex: number) => {
|
|
setPlaySettings((prev) => ({
|
|
...prev,
|
|
audioIndex,
|
|
}));
|
|
};
|
|
|
|
const selectedSubtitleStream = useMemo(() => {
|
|
return playSettings?.subtitleIndex;
|
|
}, [playSettings?.subtitleIndex]);
|
|
|
|
const setSelectedSubtitleStream = (subtitleIndex: number) => {
|
|
setPlaySettings((prev) => ({
|
|
...prev,
|
|
subtitleIndex,
|
|
}));
|
|
};
|
|
|
|
const maxBitrate = useMemo(() => {
|
|
return playSettings?.bitrate;
|
|
}, [playSettings?.bitrate]);
|
|
|
|
const setMaxBitrate = (bitrate: Bitrate | undefined) => {
|
|
console.log("setMaxBitrate", bitrate);
|
|
setPlaySettings((prev) => ({
|
|
...prev,
|
|
bitrate,
|
|
}));
|
|
};
|
|
|
|
useEffect(() => {
|
|
const subscription = ScreenOrientation.addOrientationChangeListener(
|
|
(event) => {
|
|
setOrientation(event.orientationInfo.orientation);
|
|
}
|
|
);
|
|
|
|
ScreenOrientation.getOrientationAsync().then((initialOrientation) => {
|
|
setOrientation(initialOrientation);
|
|
});
|
|
|
|
return () => {
|
|
ScreenOrientation.removeOrientationChangeListener(subscription);
|
|
};
|
|
}, []);
|
|
|
|
const [headerHeight, setHeaderHeight] = useState(350);
|
|
|
|
useImageColors({ item });
|
|
|
|
useEffect(() => {
|
|
navigation.setOptions({
|
|
headerRight: () =>
|
|
item && (
|
|
<View className="flex flex-row items-center space-x-2">
|
|
<Chromecast background="blur" width={22} height={22} />
|
|
{item.Type !== "Program" && (
|
|
<View className="flex flex-row items-center space-x-2">
|
|
<DownloadItem item={item} />
|
|
<PlayedStatus item={item} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
),
|
|
});
|
|
}, [item]);
|
|
|
|
useEffect(() => {
|
|
// If landscape
|
|
if (orientation !== ScreenOrientation.Orientation.PORTRAIT_UP) {
|
|
setHeaderHeight(230);
|
|
return;
|
|
}
|
|
|
|
if (item.Type === "Movie") setHeaderHeight(500);
|
|
else setHeaderHeight(350);
|
|
}, [item.Type, orientation]);
|
|
|
|
const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]);
|
|
|
|
const loading = useMemo(() => {
|
|
return Boolean(logoUrl && loadingLogo);
|
|
}, [loadingLogo, logoUrl]);
|
|
|
|
const insets = useSafeAreaInsets();
|
|
|
|
return (
|
|
<View
|
|
className="flex-1 relative"
|
|
style={{
|
|
paddingLeft: insets.left,
|
|
paddingRight: insets.right,
|
|
}}
|
|
>
|
|
<ParallaxScrollView
|
|
className={`flex-1 ${loading ? "opacity-0" : "opacity-100"}`}
|
|
headerHeight={headerHeight}
|
|
headerImage={
|
|
<View style={[{ flex: 1 }]}>
|
|
<ItemImage
|
|
variant={
|
|
item.Type === "Movie" && logoUrl ? "Backdrop" : "Primary"
|
|
}
|
|
item={item}
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
}}
|
|
/>
|
|
</View>
|
|
}
|
|
logo={
|
|
<>
|
|
{logoUrl ? (
|
|
<Image
|
|
source={{
|
|
uri: logoUrl,
|
|
}}
|
|
style={{
|
|
height: 130,
|
|
width: "100%",
|
|
resizeMode: "contain",
|
|
}}
|
|
onLoad={() => setLoadingLogo(false)}
|
|
onError={() => setLoadingLogo(false)}
|
|
/>
|
|
) : null}
|
|
</>
|
|
}
|
|
>
|
|
<View className="flex flex-col bg-transparent shrink">
|
|
<View className="flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink">
|
|
<ItemHeader item={item} className="mb-4" />
|
|
{item.Type !== "Program" && (
|
|
<View className="flex flex-row items-center justify-start w-full h-16">
|
|
<BitrateSelector
|
|
className="mr-1"
|
|
onChange={(val) => setMaxBitrate(val)}
|
|
selected={maxBitrate}
|
|
/>
|
|
<MediaSourceSelector
|
|
className="mr-1"
|
|
item={item}
|
|
onChange={setSelectedMediaSource}
|
|
selected={selectedMediaSource}
|
|
/>
|
|
{selectedMediaSource && (
|
|
<>
|
|
<AudioTrackSelector
|
|
className="mr-1"
|
|
source={selectedMediaSource}
|
|
onChange={setSelectedAudioStream}
|
|
selected={selectedAudioStream}
|
|
/>
|
|
<SubtitleTrackSelector
|
|
source={selectedMediaSource}
|
|
onChange={setSelectedSubtitleStream}
|
|
selected={selectedSubtitleStream}
|
|
/>
|
|
</>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
<PlayButton item={item} url={playUrl} className="grow" />
|
|
</View>
|
|
|
|
{item.Type === "Episode" && (
|
|
<SeasonEpisodesCarousel item={item} loading={loading} />
|
|
)}
|
|
|
|
<OverviewText text={item.Overview} className="px-4 my-4" />
|
|
{item.Type !== "Program" && (
|
|
<>
|
|
<CastAndCrew item={item} className="mb-4" loading={loading} />
|
|
|
|
{item.People && item.People.length > 0 && (
|
|
<View className="mb-4">
|
|
{item.People.slice(0, 3).map((person) => (
|
|
<MoreMoviesWithActor
|
|
currentItem={item}
|
|
key={person.Id}
|
|
actorId={person.Id!}
|
|
className="mb-4"
|
|
/>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{item.Type === "Episode" && (
|
|
<CurrentSeries item={item} className="mb-4" />
|
|
)}
|
|
|
|
<SimilarItems itemId={item.Id} />
|
|
</>
|
|
)}
|
|
|
|
<View className="h-16"></View>
|
|
</View>
|
|
</ParallaxScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
);
|