mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-16 01:13:27 +01:00
- present the filter sheet imperatively from the press handler (Reanimated 4 no-op via useEffect) - host filters in a single shared sheet to stop stacked sheets - keep the search input responsive on large option lists (useDeferredValue) - reset (X) also clears sort & order via a shared useFilterReset hook - per-library filter memory, reset on app close, scroll grid back to top - memoize useFilterOptions and drop debug logging
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}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|