mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Co-authored-by: lostb1t <coding-mosses0z@icloud.com> Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com> Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com> Co-authored-by: Gauvino <uruknarb20@gmail.com> Co-authored-by: storm1er <le.storm1er@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Chris <182387676+whoopsi-daisy@users.noreply.github.com> Co-authored-by: arch-fan <55891793+arch-fan@users.noreply.github.com> Co-authored-by: Alex Kim <alexkim@Alexs-MacBook-Pro.local>
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
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 { Platform, View } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
|
|
import { type Bitrate, BitrateSelector } 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 { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
|
|
import { CastAndCrew } from "@/components/series/CastAndCrew";
|
|
import { CurrentSeries } from "@/components/series/CurrentSeries";
|
|
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
|
|
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
|
import { useImageColors } from "@/hooks/useImageColors";
|
|
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 { ItemHeader } from "./ItemHeader";
|
|
import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
|
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
|
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
|
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
|
|
|
|
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
|
|
|
export type SelectedOptions = {
|
|
bitrate: Bitrate;
|
|
mediaSource: MediaSourceInfo | undefined;
|
|
audioIndex: number | undefined;
|
|
subtitleIndex: number;
|
|
};
|
|
|
|
interface ItemContentProps {
|
|
item: BaseItemDto;
|
|
isOffline: boolean;
|
|
}
|
|
|
|
export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
|
({ item, isOffline }) => {
|
|
const [api] = useAtom(apiAtom);
|
|
const [settings] = useSettings();
|
|
const { orientation } = useOrientation();
|
|
const navigation = useNavigation();
|
|
const insets = useSafeAreaInsets();
|
|
const [user] = useAtom(userAtom);
|
|
|
|
useImageColors({ 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 && (
|
|
<View className='flex flex-row items-center space-x-2'>
|
|
<Chromecast.Chromecast
|
|
background='blur'
|
|
width={22}
|
|
height={22}
|
|
/>
|
|
{item.Type !== "Program" && (
|
|
<View className='flex flex-row items-center space-x-2'>
|
|
{!Platform.isTV && (
|
|
<DownloadSingleItem item={item} size='large' />
|
|
)}
|
|
{user?.Policy?.IsAdministrator && (
|
|
<PlayInRemoteSessionButton item={item} size='large' />
|
|
)}
|
|
|
|
<PlayedStatus items={[item]} size='large' />
|
|
<AddToFavorites item={item} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
),
|
|
});
|
|
}
|
|
}, [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 (
|
|
<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)}
|
|
/>
|
|
) : (
|
|
<View />
|
|
)
|
|
}
|
|
>
|
|
<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" && !Platform.isTV && !isOffline && (
|
|
<View className='flex flex-row items-center justify-start w-full h-16'>
|
|
<BitrateSelector
|
|
className='mr-1'
|
|
onChange={(val) =>
|
|
setSelectedOptions(
|
|
(prev) => prev && { ...prev, bitrate: val },
|
|
)
|
|
}
|
|
selected={selectedOptions.bitrate}
|
|
/>
|
|
<MediaSourceSelector
|
|
className='mr-1'
|
|
item={item}
|
|
onChange={(val) =>
|
|
setSelectedOptions(
|
|
(prev) =>
|
|
prev && {
|
|
...prev,
|
|
mediaSource: val,
|
|
},
|
|
)
|
|
}
|
|
selected={selectedOptions.mediaSource}
|
|
/>
|
|
<AudioTrackSelector
|
|
className='mr-1'
|
|
source={selectedOptions.mediaSource}
|
|
onChange={(val) => {
|
|
setSelectedOptions(
|
|
(prev) =>
|
|
prev && {
|
|
...prev,
|
|
audioIndex: val,
|
|
},
|
|
);
|
|
}}
|
|
selected={selectedOptions.audioIndex}
|
|
/>
|
|
<SubtitleTrackSelector
|
|
source={selectedOptions.mediaSource}
|
|
onChange={(val) =>
|
|
setSelectedOptions(
|
|
(prev) =>
|
|
prev && {
|
|
...prev,
|
|
subtitleIndex: val,
|
|
},
|
|
)
|
|
}
|
|
selected={selectedOptions.subtitleIndex}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
<PlayButton
|
|
className='grow'
|
|
selectedOptions={selectedOptions}
|
|
item={item}
|
|
isOffline={isOffline}
|
|
/>
|
|
</View>
|
|
|
|
{item.Type === "Episode" && (
|
|
<SeasonEpisodesCarousel
|
|
item={item}
|
|
loading={loading}
|
|
isOffline={isOffline}
|
|
/>
|
|
)}
|
|
|
|
{!isOffline && (
|
|
<ItemTechnicalDetails source={selectedOptions.mediaSource} />
|
|
)}
|
|
<OverviewText text={item.Overview} className='px-4 mb-4' />
|
|
|
|
{item.Type !== "Program" && (
|
|
<>
|
|
{item.Type === "Episode" && !isOffline && (
|
|
<CurrentSeries item={item} className='mb-4' />
|
|
)}
|
|
|
|
{!isOffline && (
|
|
<CastAndCrew item={item} className='mb-4' loading={loading} />
|
|
)}
|
|
|
|
{item.People && item.People.length > 0 && !isOffline && (
|
|
<View className='mb-4'>
|
|
{item.People.slice(0, 3).map((person, idx) => (
|
|
<MoreMoviesWithActor
|
|
currentItem={item}
|
|
key={idx}
|
|
actorId={person.Id!}
|
|
className='mb-4'
|
|
/>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{!isOffline && <SimilarItems itemId={item.Id} />}
|
|
</>
|
|
)}
|
|
</View>
|
|
</ParallaxScrollView>
|
|
</View>
|
|
);
|
|
},
|
|
);
|