diff --git a/components/filters/FilterSheet.tsx b/components/filters/FilterSheet.tsx index 27e54a62..30bbe948 100644 --- a/components/filters/FilterSheet.tsx +++ b/components/filters/FilterSheet.tsx @@ -15,6 +15,7 @@ import { View, type ViewProps, } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { Button } from "../Button"; import { Input } from "../common/Input"; @@ -77,6 +78,7 @@ export const FilterSheet = ({ const bottomSheetModalRef = useRef(null); const snapPoints = useMemo(() => ["85%"], []); const { t } = useTranslation(); + const insets = useSafeAreaInsets(); const [data, setData] = useState([]); const [offset, setOffset] = useState(0); @@ -161,7 +163,13 @@ export const FilterSheet = ({ flex: 1, }} > - + {title} {t("search.x_items", { count: _data?.length })} diff --git a/components/video-player/controls/ScaleFactorSelector.tsx b/components/video-player/controls/ScaleFactorSelector.tsx index c251a59a..b6ed1853 100644 --- a/components/video-player/controls/ScaleFactorSelector.tsx +++ b/components/video-player/controls/ScaleFactorSelector.tsx @@ -1,10 +1,10 @@ import { Ionicons } from "@expo/vector-icons"; -import React from "react"; +import React, { useState } from "react"; import { Platform, TouchableOpacity } from "react-native"; +import { Text } from "@/components/common/Text"; +import { FilterSheet } from "@/components/filters/FilterSheet"; import { useHaptic } from "@/hooks/useHaptic"; -const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; - export type ScaleFactor = | 1.0 | 1.1 @@ -94,42 +94,56 @@ export const ScaleFactorSelector: React.FC = ({ disabled = false, }) => { const lightHapticFeedback = useHaptic("light"); + const [open, setOpen] = useState(false); - // Hide on TV platforms since zeego doesn't support TV - if (Platform.isTV || !DropdownMenu) return null; + // Hide on TV platforms + if (Platform.isTV) return null; const handleScaleSelect = (scale: ScaleFactor) => { onScaleChange(scale); lightHapticFeedback(); }; + const currentOption = SCALE_FACTOR_OPTIONS.find( + (option) => option.id === currentScale, + ); + return ( - - - - - - + <> + setOpen(true)} + > + + - - Scale Factor - - - {SCALE_FACTOR_OPTIONS.map((option) => ( - handleScaleSelect(option.id)} - > - {option.label} - - - ))} - - + { + const option = item as ScaleFactorOption; + return ( + option.label.toLowerCase().includes(query.toLowerCase()) || + option.description.toLowerCase().includes(query.toLowerCase()) + ); + }} + renderItemLabel={(item) => { + const option = item as ScaleFactorOption; + return {option.label}; + }} + set={(vals) => { + const chosen = vals[0] as ScaleFactorOption | undefined; + if (chosen) { + handleScaleSelect(chosen.id); + } + }} + /> + ); }; diff --git a/components/video-player/controls/VideoScalingModeSelector.tsx b/components/video-player/controls/VideoScalingModeSelector.tsx index 82874abf..00866666 100644 --- a/components/video-player/controls/VideoScalingModeSelector.tsx +++ b/components/video-player/controls/VideoScalingModeSelector.tsx @@ -1,10 +1,10 @@ import { Ionicons } from "@expo/vector-icons"; -import React from "react"; +import React, { useState } from "react"; import { Platform, TouchableOpacity } from "react-native"; +import { Text } from "@/components/common/Text"; +import { FilterSheet } from "@/components/filters/FilterSheet"; import { useHaptic } from "@/hooks/useHaptic"; -const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; - export type AspectRatio = "default" | "16:9" | "4:3" | "1:1" | "21:9"; interface AspectRatioSelectorProps { @@ -53,45 +53,56 @@ export const AspectRatioSelector: React.FC = ({ disabled = false, }) => { const lightHapticFeedback = useHaptic("light"); + const [open, setOpen] = useState(false); - // Hide on TV platforms since zeego doesn't support TV - if (Platform.isTV || !DropdownMenu) return null; + // Hide on TV platforms + if (Platform.isTV) return null; const handleRatioSelect = (ratio: AspectRatio) => { onRatioChange(ratio); lightHapticFeedback(); }; + const currentOption = ASPECT_RATIO_OPTIONS.find( + (option) => option.id === currentRatio, + ); + return ( - - - - - - + <> + setOpen(true)} + > + + - - Aspect Ratio - - - {ASPECT_RATIO_OPTIONS.map((option) => ( - handleRatioSelect(option.id)} - > - {option.label} - - {option.description} - - - - ))} - - + { + const option = item as AspectRatioOption; + return ( + option.label.toLowerCase().includes(query.toLowerCase()) || + option.description.toLowerCase().includes(query.toLowerCase()) + ); + }} + renderItemLabel={(item) => { + const option = item as AspectRatioOption; + return {option.label}; + }} + set={(vals) => { + const chosen = vals[0] as AspectRatioOption | undefined; + if (chosen) { + handleRatioSelect(chosen.id); + } + }} + /> + ); }; diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx index 39e1178b..91c84d60 100644 --- a/components/video-player/controls/dropdown/DropdownView.tsx +++ b/components/video-player/controls/dropdown/DropdownView.tsx @@ -1,11 +1,16 @@ import { Ionicons } from "@expo/vector-icons"; -import { useCallback } from "react"; -import { Platform, TouchableOpacity } from "react-native"; - -const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; - +import { + BottomSheetBackdrop, + type BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetScrollView, +} from "@gorhom/bottom-sheet"; import { useLocalSearchParams, useRouter } from "expo-router"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BITRATES } from "@/components/BitrateSelector"; +import { Text } from "@/components/common/Text"; import { useControlContext } from "../contexts/ControlContext"; import { useVideoContext } from "../contexts/VideoContext"; @@ -18,6 +23,10 @@ const DropdownView = () => { ControlContext?.mediaSource, ]; const router = useRouter(); + const insets = useSafeAreaInsets(); + const [open, setOpen] = useState(false); + const bottomSheetModalRef = useRef(null); + const snapPoints = useMemo(() => ["75%"], []); const { subtitleIndex, audioIndex, bitrateValue, playbackPosition, offline } = useLocalSearchParams<{ @@ -48,102 +57,231 @@ const DropdownView = () => { [item, mediaSource, subtitleIndex, audioIndex, playbackPosition], ); + const handleSheetChanges = useCallback((index: number) => { + if (index === -1) { + setOpen(false); + } + }, []); + + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [], + ); + + const handleOpen = () => { + setOpen(true); + bottomSheetModalRef.current?.present(); + }; + + const handleClose = () => { + setOpen(false); + bottomSheetModalRef.current?.dismiss(); + }; + + useEffect(() => { + if (open) bottomSheetModalRef.current?.present(); + else bottomSheetModalRef.current?.dismiss(); + }, [open]); + + // Hide on TV platforms + if (Platform.isTV) return null; + return ( - - - - - - - + - {!isOffline && ( - - - Quality - - - {BITRATES?.map((bitrate, idx: number) => ( - - changeBitrate(bitrate.value?.toString() ?? "") - } - > - - {bitrate.key} - - - ))} - - - )} - - - Subtitle - - + + + + + - {subtitleTracks?.map((sub, idx: number) => ( - sub.setTrack()} - > - - {sub.name} - - - ))} - - - {(audioTracks?.length ?? 0) > 0 && ( - - - Audio - - - {audioTracks?.map((track, idx: number) => ( - track.setTrack()} + Playback Options + + {/* Quality Section */} + {!isOffline && ( + + + Quality + + - - {track.name} - - - ))} - - - )} - - + {BITRATES?.map((bitrate, idx: number) => ( + + { + changeBitrate(bitrate.value?.toString() ?? ""); + setTimeout(() => handleClose(), 250); + }} + className='bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between' + > + {bitrate.key} + {bitrateValue === (bitrate.value?.toString() ?? "") ? ( + + ) : ( + + )} + + {idx < BITRATES.length - 1 && ( + + )} + + ))} + + + )} + + {/* Subtitle Section */} + + + Subtitles + + + {subtitleTracks?.map((sub, idx: number) => ( + + { + sub.setTrack(); + setTimeout(() => handleClose(), 250); + }} + className='bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between' + > + {sub.name} + {subtitleIndex === sub.index.toString() ? ( + + ) : ( + + )} + + {idx < (subtitleTracks?.length ?? 0) - 1 && ( + + )} + + ))} + + + + {/* Audio Section */} + {(audioTracks?.length ?? 0) > 0 && ( + + + Audio + + + {audioTracks?.map((track, idx: number) => ( + + { + track.setTrack(); + setTimeout(() => handleClose(), 250); + }} + className='bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between' + > + {track.name} + {audioIndex === track.index.toString() ? ( + + ) : ( + + )} + + {idx < (audioTracks?.length ?? 0) - 1 && ( + + )} + + ))} + + + )} + + + + ); };