mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
Refactor page.tsx to use local search params
This commit is contained in:
@@ -1,319 +1,99 @@
|
|||||||
import type {
|
import { useLocalSearchParams } from "expo-router";
|
||||||
BaseItemDto,
|
import type React from "react";
|
||||||
MediaSourceInfo,
|
import { useEffect } from "react";
|
||||||
} 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 { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import Animated, {
|
||||||
import { type Bitrate } from "@/components/BitrateSelector";
|
runOnJS,
|
||||||
import { ItemImage } from "@/components/common/ItemImage";
|
useAnimatedStyle,
|
||||||
import { DownloadSingleItem } from "@/components/DownloadItem";
|
useSharedValue,
|
||||||
import { OverviewText } from "@/components/OverviewText";
|
withTiming,
|
||||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
} from "react-native-reanimated";
|
||||||
// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
|
import { Text } from "@/components/common/Text";
|
||||||
import { PlayButton } from "@/components/PlayButton";
|
import { ItemContent } from "@/components/ItemContent";
|
||||||
import { PlayedStatus } from "@/components/PlayedStatus";
|
import { useItemQuery } from "@/hooks/useItemQuery";
|
||||||
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 Chromecast = !Platform.isTV ? require("./Chromecast") : null;
|
const Page: React.FC = () => {
|
||||||
|
const { id } = useLocalSearchParams() as { id: string };
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
export type SelectedOptions = {
|
const { offline } = useLocalSearchParams() as { offline?: string };
|
||||||
bitrate: Bitrate;
|
const isOffline = offline === "true";
|
||||||
mediaSource: MediaSourceInfo | undefined;
|
|
||||||
audioIndex: number | undefined;
|
|
||||||
subtitleIndex: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ItemContentProps {
|
const { data: item, isError } = useItemQuery(itemId, false, undefined, [ItemFields.MediaSources]);
|
||||||
item: BaseItemDto;
|
const { data: mediaSourcesitem, isError } = useItemQuery(id, isOffline);
|
||||||
isOffline: boolean;
|
|
||||||
mediaSourcesitem: BaseItemDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
const opacity = useSharedValue(1);
|
||||||
({ item, isOffline, mediaSourcesitem }) => {
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
const [api] = useAtom(apiAtom);
|
return {
|
||||||
const { settings } = useSettings();
|
opacity: opacity.value,
|
||||||
const { orientation } = useOrientation();
|
};
|
||||||
const navigation = useNavigation();
|
});
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const [user] = useAtom(userAtom);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const itemColors = useImageColorsReturn({ item });
|
const fadeOut = (callback: any) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
opacity.value = withTiming(0, { duration: 500 }, (finished) => {
|
||||||
|
if (finished) {
|
||||||
|
runOnJS(callback)();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
const [loadingLogo, setLoadingLogo] = useState(true);
|
const fadeIn = (callback: any) => {
|
||||||
const [headerHeight, setHeaderHeight] = useState(350);
|
setTimeout(() => {
|
||||||
|
opacity.value = withTiming(1, { duration: 500 }, (finished) => {
|
||||||
|
if (finished) {
|
||||||
|
runOnJS(callback)();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
const [selectedOptions, setSelectedOptions] = useState<
|
useEffect(() => {
|
||||||
SelectedOptions | undefined
|
if (item) {
|
||||||
>(undefined);
|
fadeOut(() => {});
|
||||||
|
} else {
|
||||||
const {
|
fadeIn(() => {});
|
||||||
defaultAudioIndex,
|
}
|
||||||
defaultBitrate,
|
}, [item]);
|
||||||
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" ? (
|
|
||||||
<View className='flex flex-row items-center pl-2'>
|
|
||||||
<Chromecast.Chromecast width={22} height={22} />
|
|
||||||
{item.Type !== "Program" && (
|
|
||||||
<View className='flex flex-row items-center'>
|
|
||||||
{!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>
|
|
||||||
) : (
|
|
||||||
<View className='flex flex-row items-center space-x-2'>
|
|
||||||
<Chromecast.Chromecast 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;
|
|
||||||
|
|
||||||
|
if (isError)
|
||||||
return (
|
return (
|
||||||
<View
|
<View className='flex flex-col items-center justify-center h-screen w-screen'>
|
||||||
className='flex-1 relative'
|
<Text>{t("item_card.could_not_load_item")}</Text>
|
||||||
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%",
|
|
||||||
}}
|
|
||||||
contentFit='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-2' />
|
|
||||||
{item.Type !== "Program" && !Platform.isTV && !isOffline && (
|
|
||||||
<View className='flex flex-row items-center justify-start w-full h-16'>
|
|
||||||
<BitrateSheet
|
|
||||||
className='mr-1'
|
|
||||||
onChange={(val) =>
|
|
||||||
setSelectedOptions(
|
|
||||||
(prev) => prev && { ...prev, bitrate: val },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
selected={selectedOptions.bitrate}
|
|
||||||
/>
|
|
||||||
<MediaSourceSheet
|
|
||||||
className='mr-1'
|
|
||||||
item={mediaSourcesitem}
|
|
||||||
onChange={(val) =>
|
|
||||||
setSelectedOptions(
|
|
||||||
(prev) =>
|
|
||||||
prev && {
|
|
||||||
...prev,
|
|
||||||
mediaSource: val,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
selected={selectedOptions.mediaSource}
|
|
||||||
/>
|
|
||||||
<TrackSheet
|
|
||||||
className='mr-1'
|
|
||||||
streamType='Audio'
|
|
||||||
title={t("item_card.audio")}
|
|
||||||
source={selectedOptions.mediaSource}
|
|
||||||
onChange={(val) => {
|
|
||||||
setSelectedOptions(
|
|
||||||
(prev) =>
|
|
||||||
prev && {
|
|
||||||
...prev,
|
|
||||||
audioIndex: val,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
selected={selectedOptions.audioIndex}
|
|
||||||
/>
|
|
||||||
<TrackSheet
|
|
||||||
source={selectedOptions.mediaSource}
|
|
||||||
streamType='Subtitle'
|
|
||||||
title={t("item_card.subtitles")}
|
|
||||||
onChange={(val) =>
|
|
||||||
setSelectedOptions(
|
|
||||||
(prev) =>
|
|
||||||
prev && {
|
|
||||||
...prev,
|
|
||||||
subtitleIndex: val,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
selected={selectedOptions.subtitleIndex}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PlayButton
|
|
||||||
className='grow'
|
|
||||||
selectedOptions={selectedOptions}
|
|
||||||
item={mediaSourcesitem}
|
|
||||||
isOffline={isOffline}
|
|
||||||
colors={itemColors}
|
|
||||||
/>
|
|
||||||
</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>
|
</View>
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
return (
|
||||||
|
<View className='flex flex-1 relative'>
|
||||||
|
<Animated.View
|
||||||
|
pointerEvents={"none"}
|
||||||
|
style={[animatedStyle]}
|
||||||
|
className='absolute top-0 left-0 flex flex-col items-start h-screen w-screen px-4 z-50 bg-black'
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
height: item?.Type === "Episode" ? 300 : 450,
|
||||||
|
}}
|
||||||
|
className='bg-transparent rounded-lg mb-4 w-full'
|
||||||
|
/>
|
||||||
|
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
|
||||||
|
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
|
||||||
|
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
|
||||||
|
<View className='flex flex-row space-x-1 mb-8'>
|
||||||
|
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||||
|
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||||
|
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||||
|
</View>
|
||||||
|
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
|
||||||
|
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2' />
|
||||||
|
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
|
||||||
|
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
|
||||||
|
</Animated.View>
|
||||||
|
{item && <ItemContent item={item} isOffline={isOffline} mediaSourcesItem={mediaSourcesItem} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|||||||
Reference in New Issue
Block a user