mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
feat: add technical details to item
This commit is contained in:
@@ -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(
|
||||
<SeasonEpisodesCarousel item={item} loading={loading} />
|
||||
)}
|
||||
|
||||
<OverviewText text={item.Overview} className="px-4 my-4" />
|
||||
<ItemTechnicalDetails source={selectedOptions.mediaSource} />
|
||||
<OverviewText text={item.Overview} className="px-4 mb-4" />
|
||||
|
||||
{item.Type !== "Program" && (
|
||||
<>
|
||||
{item.Type === "Episode" && (
|
||||
|
||||
236
components/ItemTechnicalDetails.tsx
Normal file
236
components/ItemTechnicalDetails.tsx
Normal file
@@ -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<Props> = ({ source, ...props }) => {
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
|
||||
return (
|
||||
<View className="px-4 mt-2 mb-4">
|
||||
<Text className="text-lg font-bold mb-4">Video</Text>
|
||||
<TouchableOpacity onPress={() => bottomSheetModalRef.current?.present()}>
|
||||
<View className="flex flex-row space-x-2">
|
||||
<VideoStreamInfo source={source} />
|
||||
</View>
|
||||
<Text className="text-purple-600">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">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">Audio</Text>
|
||||
<AudioStreamInfo
|
||||
audioStreams={
|
||||
source?.MediaStreams?.filter(
|
||||
(stream) => stream.Type === "Audio"
|
||||
) || []
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="">
|
||||
<Text className="text-lg font-bold mb-2">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 className="flex flex-col">
|
||||
<Text className="mb-3 text-neutral-400 text-xs">
|
||||
{audioStreams.DisplayTitle}
|
||||
</Text>
|
||||
<View key={index} 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]);
|
||||
|
||||
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 = 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];
|
||||
};
|
||||
Reference in New Issue
Block a user