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 && }
+ >
+ )}
+
+
+
+ );
+ },
+);