mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-28 00:30:30 +01:00
Each FilterButton owned its own bottom-sheet modal, so tapping two filter buttons quickly could present two sheets at once. Add a FilterSheetProvider that hosts one shared sheet per screen; buttons just swap its content via openFilter(). A first-wins guard in the provider (one source of truth, reset on the single close path) ignores a second tap while a sheet is opening. The generic FilterSheet is untouched, so the bitrate/source/track selectors and provider-less screens (logs, discover) keep their standalone sheet. Also drop the header deps that remounted the whole filter bar on every fetch.
150 lines
4.0 KiB
TypeScript
150 lines
4.0 KiB
TypeScript
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
|
import type { BottomSheetModal } from "@gorhom/bottom-sheet";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useCallback, useRef, useState } from "react";
|
|
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
|
import { Text } from "@/components/common/Text";
|
|
import { FilterSheet } from "./FilterSheet";
|
|
import { useFilterSheet } from "./FilterSheetProvider";
|
|
|
|
interface FilterButtonProps<T> extends ViewProps {
|
|
id: string;
|
|
disableSearch?: boolean;
|
|
queryKey: string;
|
|
values: T[];
|
|
title: string;
|
|
set: (value: T[]) => void;
|
|
queryFn: (params: any) => Promise<any>;
|
|
searchFilter?: (item: T, query: string) => boolean;
|
|
renderItemLabel: (item: T) => React.ReactNode;
|
|
multiple?: boolean;
|
|
icon?: "filter" | "sort";
|
|
}
|
|
|
|
export const FilterButton = <T,>({
|
|
id,
|
|
queryFn,
|
|
queryKey,
|
|
set,
|
|
values, // selected values
|
|
title,
|
|
renderItemLabel,
|
|
searchFilter,
|
|
disableSearch = false,
|
|
multiple = false,
|
|
icon = "filter",
|
|
...props
|
|
}: FilterButtonProps<T>) => {
|
|
// When a FilterSheetProvider is present (library / collections), all buttons
|
|
// share one sheet so two can never stack. Outside a provider (e.g. logs,
|
|
// discover), fall back to this button's own standalone sheet.
|
|
const shared = useFilterSheet();
|
|
|
|
const { data: filters, isLoading } = useQuery<T[]>({
|
|
queryKey: ["filters", title, queryKey, id],
|
|
queryFn,
|
|
staleTime: 0,
|
|
enabled: !!id && !!queryFn && !!queryKey,
|
|
});
|
|
|
|
// Standalone-mode state (unused in shared mode).
|
|
const [open, setOpen] = useState(false);
|
|
const sheetModalRef = useRef<BottomSheetModal | null>(null);
|
|
|
|
const onButtonPress = useCallback(() => {
|
|
if (shared) {
|
|
shared.openFilter({
|
|
key: `${id}:${queryKey}`,
|
|
id,
|
|
queryKey,
|
|
queryFn,
|
|
title,
|
|
values: values as unknown[],
|
|
set: set as (value: unknown[]) => void,
|
|
renderItemLabel: renderItemLabel as (item: unknown) => React.ReactNode,
|
|
searchFilter: searchFilter as
|
|
| ((item: unknown, query: string) => boolean)
|
|
| undefined,
|
|
disableSearch,
|
|
multiple,
|
|
});
|
|
return;
|
|
}
|
|
// present() must run from the press handler: from an effect after a state
|
|
// update it silently no-ops on the new architecture and the sheet never
|
|
// appears.
|
|
setOpen(true);
|
|
sheetModalRef.current?.present();
|
|
}, [
|
|
shared,
|
|
id,
|
|
queryKey,
|
|
queryFn,
|
|
title,
|
|
values,
|
|
set,
|
|
renderItemLabel,
|
|
searchFilter,
|
|
disableSearch,
|
|
multiple,
|
|
]);
|
|
|
|
return (
|
|
<>
|
|
<TouchableOpacity onPress={onButtonPress}>
|
|
<View
|
|
className={`
|
|
px-3 py-1.5 rounded-full flex flex-row items-center space-x-1
|
|
${
|
|
values.length > 0
|
|
? "bg-purple-600 border border-purple-700"
|
|
: "bg-neutral-900 border border-neutral-900"
|
|
}
|
|
${filters?.length === 0 && "opacity-50"}
|
|
`}
|
|
{...props}
|
|
>
|
|
<Text
|
|
className={`
|
|
${values.length > 0 ? "text-purple-100" : "text-neutral-100"}
|
|
text-xs font-semibold`}
|
|
>
|
|
{title}
|
|
</Text>
|
|
{icon === "filter" ? (
|
|
<Ionicons
|
|
name='filter'
|
|
size={14}
|
|
color='white'
|
|
style={{ opacity: 0.5 }}
|
|
/>
|
|
) : (
|
|
<FontAwesome
|
|
name='sort'
|
|
size={14}
|
|
color='white'
|
|
style={{ opacity: 0.5 }}
|
|
/>
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
{!shared && (
|
|
<FilterSheet<T>
|
|
title={title}
|
|
open={open}
|
|
setOpen={setOpen}
|
|
modalRef={sheetModalRef}
|
|
loading={isLoading}
|
|
data={filters}
|
|
values={values}
|
|
set={set}
|
|
renderItemLabel={renderItemLabel}
|
|
searchFilter={searchFilter}
|
|
disableSearch={disableSearch}
|
|
multiple={multiple}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|