diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 3044b5d3..81142e54 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -34,6 +34,7 @@ import { ItemHeader } from "./ItemHeader"; import { MediaSourceSelector } from "./MediaSourceSelector"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; import { SubtitleHelper } from "@/utils/SubtitleHelper"; +import { ItemTechnicalDetails } from "./ItemTechnicalDetails"; export type SelectedOptions = { bitrate: Bitrate; @@ -258,7 +259,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - + + + {item.Type !== "Program" && ( <> {item.Type === "Episode" && ( diff --git a/components/ItemTechnicalDetails.tsx b/components/ItemTechnicalDetails.tsx new file mode 100644 index 00000000..2b0785a9 --- /dev/null +++ b/components/ItemTechnicalDetails.tsx @@ -0,0 +1,236 @@ +import { Ionicons } from "@expo/vector-icons"; +import { + MediaSourceInfo, + type MediaStream, +} from "@jellyfin/sdk/lib/generated-client"; +import React, { useMemo, useRef } from "react"; +import { TouchableOpacity, View } from "react-native"; +import { Badge } from "./Badge"; +import { Text } from "./common/Text"; +import { + BottomSheetModal, + BottomSheetBackdropProps, + BottomSheetBackdrop, + BottomSheetView, + BottomSheetScrollView, +} from "@gorhom/bottom-sheet"; +import { Button } from "./Button"; + +interface Props { + source?: MediaSourceInfo; +} + +export const ItemTechnicalDetails: React.FC = ({ source, ...props }) => { + const bottomSheetModalRef = useRef(null); + + return ( + + Video + bottomSheetModalRef.current?.present()}> + + + + More details + + ( + + )} + > + + + + Video + + + + + + + Audio + stream.Type === "Audio" + ) || [] + } + /> + + + + Subtitles + stream.Type === "Subtitle" + ) || [] + } + /> + + + + + + ); +}; + +const SubtitleStreamInfo = ({ + subtitleStreams, +}: { + subtitleStreams: MediaStream[]; +}) => { + return ( + + {subtitleStreams.map((stream, index) => ( + + + {stream.DisplayTitle} + + + + } + text={stream.Language} + /> + + } + /> + + + ))} + + ); +}; + +const AudioStreamInfo = ({ audioStreams }: { audioStreams: MediaStream[] }) => { + return ( + + {audioStreams.map((audioStreams, index) => ( + + + {audioStreams.DisplayTitle} + + + + } + text={audioStreams.Language} + /> + + } + text={audioStreams.Codec} + /> + } + text={audioStreams.ChannelLayout} + /> + + } + text={formatBitrate(audioStreams.BitRate)} + /> + + + ))} + + ); +}; + +const VideoStreamInfo = ({ source }: { source?: MediaSourceInfo }) => { + if (!source) return null; + + const videoStream = useMemo(() => { + return source.MediaStreams?.find( + (stream) => stream.Type === "Video" + ) as MediaStream; + }, [source.MediaStreams]); + + return ( + + } + text={formatFileSize(source.Size)} + /> + } + text={`${videoStream.Width}x${videoStream.Height}`} + /> + + } + text={videoStream.VideoRange} + /> + + } + text={videoStream.Codec} + /> + + } + text={formatBitrate(videoStream.BitRate)} + /> + } + text={`${videoStream.AverageFrameRate?.toFixed(0)} fps`} + /> + + ); +}; + +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 = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString()); + return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i]; +}; + +const formatBitrate = (bitrate?: number | null) => { + if (!bitrate) return "N/A"; + + const sizes = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]; + if (bitrate === 0) return "0 bps"; + const i = parseInt(Math.floor(Math.log(bitrate) / Math.log(1000)).toString()); + return Math.round((bitrate / Math.pow(1000, i)) * 100) / 100 + " " + sizes[i]; +};