mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-12 17:00:23 +01:00
On the new architecture with Reanimated 4, BottomSheetModal.present() called from a useEffect after a state update silently no-ops: the press registered, open flipped to true, the effect called present() on a valid ref - and nothing mounted (no onChange, nothing in the native tree). Sheets that present() directly inside their press handler (downloads, account picker) kept working, which is what pinned it down. FilterSheet now takes a modalRef and the opener presents imperatively from the gesture handler. The [open] effect only handles closing, and never dismisses a modal that was never presented. The sheet also opens immediately with a loader while options load, instead of the old data-loaded press gate that left the button silently dead. This restores genre/year/tag/sort filters in libraries and collections, and the same pattern is applied to the bitrate/media-source/track sheets that share FilterSheet.
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import type { BottomSheetModal } from "@gorhom/bottom-sheet";
|
|
import type {
|
|
BaseItemDto,
|
|
MediaSourceInfo,
|
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Platform, TouchableOpacity, View } from "react-native";
|
|
import { Text } from "./common/Text";
|
|
import { FilterSheet } from "./filters/FilterSheet";
|
|
|
|
interface Props extends React.ComponentProps<typeof View> {
|
|
item: BaseItemDto;
|
|
onChange: (value: MediaSourceInfo) => void;
|
|
selected?: MediaSourceInfo | null;
|
|
}
|
|
|
|
export const MediaSourceSheet: React.FC<Props> = ({
|
|
item,
|
|
onChange,
|
|
selected,
|
|
...props
|
|
}) => {
|
|
const isTv = Platform.isTV;
|
|
const { t } = useTranslation();
|
|
const [open, setOpen] = useState(false);
|
|
const sheetModalRef = useRef<BottomSheetModal | null>(null);
|
|
|
|
const getDisplayName = useCallback((source: MediaSourceInfo) => {
|
|
const videoStream = source.MediaStreams?.find((x) => x.Type === "Video");
|
|
if (source.Name) return source.Name;
|
|
if (videoStream?.DisplayTitle) return videoStream.DisplayTitle;
|
|
return `Source ${source.Id}`;
|
|
}, []);
|
|
|
|
const selectedName = useMemo(() => {
|
|
if (!selected) return "";
|
|
return getDisplayName(selected);
|
|
}, [selected, getDisplayName]);
|
|
|
|
if (isTv || (item.MediaSources && item.MediaSources.length <= 1)) return null;
|
|
|
|
return (
|
|
<View className='flex shrink' style={{ minWidth: 75 }}>
|
|
<View className='flex flex-col' {...props}>
|
|
<Text className='opacity-50 mb-1 text-xs'>{t("item_card.video")}</Text>
|
|
<TouchableOpacity
|
|
className='bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center'
|
|
onPress={() => {
|
|
setOpen(true);
|
|
sheetModalRef.current?.present();
|
|
}}
|
|
>
|
|
<Text numberOfLines={1}>{selectedName}</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<FilterSheet
|
|
open={open}
|
|
setOpen={setOpen}
|
|
modalRef={sheetModalRef}
|
|
title={t("item_card.video")}
|
|
data={item.MediaSources || []}
|
|
values={selected ? [selected] : []}
|
|
multiple={false}
|
|
searchFilter={(src, query) =>
|
|
getDisplayName(src as MediaSourceInfo)
|
|
.toLowerCase()
|
|
.includes(query.toLowerCase())
|
|
}
|
|
renderItemLabel={(src) => (
|
|
<Text>{getDisplayName(src as MediaSourceInfo)}</Text>
|
|
)}
|
|
set={(vals) => {
|
|
const chosen = vals[0] as MediaSourceInfo | undefined;
|
|
if (chosen) onChange(chosen);
|
|
}}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|