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.
106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
import type { BottomSheetModal } from "@gorhom/bottom-sheet";
|
|
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
|
|
import { 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> {
|
|
source?: MediaSourceInfo;
|
|
onChange: (value: number) => void;
|
|
selected?: number | undefined;
|
|
streamType?: string;
|
|
title: string;
|
|
}
|
|
|
|
export const TrackSheet: React.FC<Props> = ({
|
|
source,
|
|
onChange,
|
|
selected,
|
|
streamType,
|
|
title,
|
|
...props
|
|
}) => {
|
|
const isTv = Platform.isTV;
|
|
const { t } = useTranslation();
|
|
|
|
const streams = useMemo(
|
|
() => source?.MediaStreams?.filter((x) => x.Type === streamType),
|
|
[source],
|
|
);
|
|
|
|
const selectedSteam = useMemo(
|
|
() => streams?.find((x) => x.Index === selected),
|
|
[streams, selected],
|
|
);
|
|
|
|
const noneOption = useMemo(
|
|
() => ({ Index: -1, DisplayTitle: t("common.none") }),
|
|
[t],
|
|
);
|
|
|
|
// Creates a modified data array that includes a "None" option for subtitles
|
|
// We might want to possibly do this for other places, like audio?
|
|
const addNoneToSubtitles = useMemo(() => {
|
|
if (streamType === "Subtitle") {
|
|
const result = streams ? [noneOption, ...streams] : [noneOption];
|
|
return result;
|
|
}
|
|
return streams;
|
|
}, [streams, streamType, noneOption]);
|
|
const [open, setOpen] = useState(false);
|
|
const sheetModalRef = useRef<BottomSheetModal | null>(null);
|
|
|
|
if (isTv || (streams && streams.length === 0)) return null;
|
|
|
|
return (
|
|
<View className='flex shrink' style={{ minWidth: 60 }} {...props}>
|
|
<View className='flex flex-col'>
|
|
<Text className='opacity-50 mb-1 text-xs'>{title}</Text>
|
|
<TouchableOpacity
|
|
className='bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between'
|
|
onPress={() => {
|
|
setOpen(true);
|
|
sheetModalRef.current?.present();
|
|
}}
|
|
>
|
|
<Text numberOfLines={1}>
|
|
{selected === -1 && streamType === "Subtitle"
|
|
? t("common.none")
|
|
: selectedSteam?.DisplayTitle || t("common.select")}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<FilterSheet
|
|
open={open}
|
|
setOpen={setOpen}
|
|
modalRef={sheetModalRef}
|
|
title={title}
|
|
data={addNoneToSubtitles || []}
|
|
values={
|
|
selected === -1 && streamType === "Subtitle"
|
|
? [{ Index: -1, DisplayTitle: t("common.none") }]
|
|
: selectedSteam
|
|
? [selectedSteam]
|
|
: []
|
|
}
|
|
multiple={false}
|
|
searchFilter={(item, query) => {
|
|
const label = (item as any).DisplayTitle || "";
|
|
return label.toLowerCase().includes(query.toLowerCase());
|
|
}}
|
|
renderItemLabel={(item) => (
|
|
<Text>{(item as any).DisplayTitle || ""}</Text>
|
|
)}
|
|
set={(vals) => {
|
|
const chosen = vals[0] as any;
|
|
if (chosen && chosen.Index !== null && chosen.Index !== undefined) {
|
|
onChange(chosen.Index);
|
|
}
|
|
}}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|