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.
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
import type { BottomSheetModal } from "@gorhom/bottom-sheet";
|
|
import {
|
|
createContext,
|
|
type PropsWithChildren,
|
|
useCallback,
|
|
useContext,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { type FilterConfig, SharedFilterSheet } from "./SharedFilterSheet";
|
|
|
|
interface FilterSheetContextType {
|
|
openFilter: (config: FilterConfig) => void;
|
|
}
|
|
|
|
const FilterSheetContext = createContext<FilterSheetContextType | null>(null);
|
|
|
|
/**
|
|
* Returns the shared-sheet controller, or null when rendered outside a
|
|
* FilterSheetProvider — FilterButton then falls back to its own standalone
|
|
* sheet (used by screens that don't host a provider, e.g. logs / discover).
|
|
*/
|
|
export const useFilterSheet = (): FilterSheetContextType | null =>
|
|
useContext(FilterSheetContext);
|
|
|
|
/**
|
|
* Hosts the single shared filter sheet for a screen. Every FilterButton under
|
|
* it calls openFilter() to show its options in that one sheet — so two sheets
|
|
* can never stack regardless of how fast the buttons are tapped. present() runs
|
|
* synchronously from the button's press handler (the modal is always mounted).
|
|
*/
|
|
export const FilterSheetProvider: React.FC<PropsWithChildren> = ({
|
|
children,
|
|
}) => {
|
|
const modalRef = useRef<BottomSheetModal | null>(null);
|
|
const [open, setOpen] = useState(false);
|
|
const [config, setConfig] = useState<FilterConfig | null>(null);
|
|
|
|
// First-wins guard. With a single shared sheet there is exactly one source of
|
|
// truth (this ref) reset on the one close path — so unlike a per-button guard
|
|
// it can't get stuck on remounts or multiple instances. A second tap during
|
|
// the first sheet's open animation is ignored; the first tapped filter wins.
|
|
const openRef = useRef(false);
|
|
|
|
const openFilter = useCallback((next: FilterConfig) => {
|
|
if (openRef.current) return;
|
|
openRef.current = true;
|
|
setConfig(next);
|
|
setOpen(true);
|
|
modalRef.current?.present();
|
|
}, []);
|
|
|
|
// Single close path for every dismissal (select / swipe / backdrop) — frees
|
|
// the guard reliably.
|
|
const closeSheet = useCallback(() => {
|
|
openRef.current = false;
|
|
setOpen(false);
|
|
}, []);
|
|
|
|
const value = useMemo(() => ({ openFilter }), [openFilter]);
|
|
|
|
return (
|
|
<FilterSheetContext.Provider value={value}>
|
|
{children}
|
|
<SharedFilterSheet
|
|
modalRef={modalRef}
|
|
open={open}
|
|
setOpen={closeSheet}
|
|
config={config}
|
|
/>
|
|
</FilterSheetContext.Provider>
|
|
);
|
|
};
|