forked from Ninjalama/streamyfin_mirror
242 lines
7.0 KiB
TypeScript
242 lines
7.0 KiB
TypeScript
import { formatBitrate } from "@/utils/bitrate";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import {
|
|
BottomSheetBackdrop,
|
|
type BottomSheetBackdropProps,
|
|
BottomSheetModal,
|
|
BottomSheetScrollView,
|
|
BottomSheetView,
|
|
} from "@gorhom/bottom-sheet";
|
|
import type {
|
|
MediaSourceInfo,
|
|
MediaStream,
|
|
} from "@jellyfin/sdk/lib/generated-client";
|
|
import type React from "react";
|
|
import { useMemo, useRef } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { TouchableOpacity, View } from "react-native";
|
|
import { Badge } from "./Badge";
|
|
import { Button } from "./Button";
|
|
import { Text } from "./common/Text";
|
|
|
|
interface Props {
|
|
source?: MediaSourceInfo;
|
|
}
|
|
|
|
export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
|
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<View className='px-4 mt-2 mb-4'>
|
|
<Text className='text-lg font-bold mb-4'>{t("item_card.video")}</Text>
|
|
<TouchableOpacity onPress={() => bottomSheetModalRef.current?.present()}>
|
|
<View className='flex flex-row space-x-2'>
|
|
<VideoStreamInfo source={source} />
|
|
</View>
|
|
<Text className='text-purple-600'>{t("item_card.more_details")}</Text>
|
|
</TouchableOpacity>
|
|
<BottomSheetModal
|
|
ref={bottomSheetModalRef}
|
|
snapPoints={["80%"]}
|
|
handleIndicatorStyle={{
|
|
backgroundColor: "white",
|
|
}}
|
|
backgroundStyle={{
|
|
backgroundColor: "#171717",
|
|
}}
|
|
backdropComponent={(props: BottomSheetBackdropProps) => (
|
|
<BottomSheetBackdrop
|
|
{...props}
|
|
disappearsOnIndex={-1}
|
|
appearsOnIndex={0}
|
|
/>
|
|
)}
|
|
>
|
|
<BottomSheetScrollView>
|
|
<View className='flex flex-col space-y-2 p-4 mb-4'>
|
|
<View className=''>
|
|
<Text className='text-lg font-bold mb-4'>
|
|
{t("item_card.video")}
|
|
</Text>
|
|
<View className='flex flex-row space-x-2'>
|
|
<VideoStreamInfo source={source} />
|
|
</View>
|
|
</View>
|
|
|
|
<View className=''>
|
|
<Text className='text-lg font-bold mb-2'>
|
|
{t("item_card.audio")}
|
|
</Text>
|
|
<AudioStreamInfo
|
|
audioStreams={
|
|
source?.MediaStreams?.filter(
|
|
(stream) => stream.Type === "Audio",
|
|
) || []
|
|
}
|
|
/>
|
|
</View>
|
|
|
|
<View className=''>
|
|
<Text className='text-lg font-bold mb-2'>
|
|
{t("item_card.subtitles")}
|
|
</Text>
|
|
<SubtitleStreamInfo
|
|
subtitleStreams={
|
|
source?.MediaStreams?.filter(
|
|
(stream) => stream.Type === "Subtitle",
|
|
) || []
|
|
}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</BottomSheetScrollView>
|
|
</BottomSheetModal>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const SubtitleStreamInfo = ({
|
|
subtitleStreams,
|
|
}: {
|
|
subtitleStreams: MediaStream[];
|
|
}) => {
|
|
return (
|
|
<View className='flex flex-col'>
|
|
{subtitleStreams.map((stream, index) => (
|
|
<View key={stream.Index} className='flex flex-col'>
|
|
<Text className='text-xs mb-3 text-neutral-400'>
|
|
{stream.DisplayTitle}
|
|
</Text>
|
|
<View className='flex flex-row flex-wrap gap-2'>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={
|
|
<Ionicons name='language-outline' size={16} color='white' />
|
|
}
|
|
text={stream.Language}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
text={stream.Codec}
|
|
iconLeft={
|
|
<Ionicons name='layers-outline' size={16} color='white' />
|
|
}
|
|
/>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const AudioStreamInfo = ({ audioStreams }: { audioStreams: MediaStream[] }) => {
|
|
return (
|
|
<View className='flex flex-col'>
|
|
{audioStreams.map((audioStreams, index) => (
|
|
<View key={index} className='flex flex-col'>
|
|
<Text className='mb-3 text-neutral-400 text-xs'>
|
|
{audioStreams.DisplayTitle}
|
|
</Text>
|
|
<View className='flex-row flex-wrap gap-2'>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={
|
|
<Ionicons name='language-outline' size={16} color='white' />
|
|
}
|
|
text={audioStreams.Language}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={
|
|
<Ionicons
|
|
name='musical-notes-outline'
|
|
size={16}
|
|
color='white'
|
|
/>
|
|
}
|
|
text={audioStreams.Codec}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={<Ionicons name='mic-outline' size={16} color='white' />}
|
|
text={audioStreams.ChannelLayout}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={
|
|
<Ionicons name='speedometer-outline' size={16} color='white' />
|
|
}
|
|
text={formatBitrate(audioStreams.BitRate)}
|
|
/>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => {
|
|
if (!source) return null;
|
|
|
|
const videoStream = useMemo(() => {
|
|
return source.MediaStreams?.find(
|
|
(stream) => stream.Type === "Video",
|
|
) as MediaStream;
|
|
}, [source.MediaStreams]);
|
|
|
|
if (!videoStream) return null;
|
|
|
|
return (
|
|
<View className='flex-row flex-wrap gap-2'>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={<Ionicons name='film-outline' size={16} color='white' />}
|
|
text={formatFileSize(source.Size)}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={<Ionicons name='film-outline' size={16} color='white' />}
|
|
text={`${videoStream.Width}x${videoStream.Height}`}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={
|
|
<Ionicons name='color-palette-outline' size={16} color='white' />
|
|
}
|
|
text={videoStream.VideoRange}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={
|
|
<Ionicons name='code-working-outline' size={16} color='white' />
|
|
}
|
|
text={videoStream.Codec}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={
|
|
<Ionicons name='speedometer-outline' size={16} color='white' />
|
|
}
|
|
text={formatBitrate(videoStream.BitRate)}
|
|
/>
|
|
<Badge
|
|
variant='gray'
|
|
iconLeft={<Ionicons name='play-outline' size={16} color='white' />}
|
|
text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const formatFileSize = (bytes?: number | null) => {
|
|
if (!bytes) return "N/A";
|
|
|
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
if (bytes === 0) return "0 Byte";
|
|
const i = Number.parseInt(
|
|
Math.floor(Math.log(bytes) / Math.log(1024)).toString(),
|
|
);
|
|
return `${Math.round((bytes / 1024 ** i) * 100) / 100} ${sizes[i]}`;
|
|
};
|