fix: modal for android + dropdown for ios

This commit is contained in:
Fredrik Burmester
2025-09-30 15:23:15 +02:00
parent 8407124464
commit ab472bab6e
10 changed files with 129 additions and 119 deletions

View File

@@ -2,7 +2,6 @@ import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import { useCallback } from "react";
import { useGlobalModal } from "@/providers/GlobalModalProvider";
@@ -66,7 +65,7 @@ export const GlobalModal = () => {
enablePanDownToClose={modalOptions.enablePanDownToClose}
enableDismissOnClose
>
<BottomSheetView>{modalState.content}</BottomSheetView>
{modalState.content}
</BottomSheetModal>
);
};

View File

@@ -1,5 +1,6 @@
import { Button, ContextMenu, Host, Picker } from "@expo/ui/swift-ui";
import { Ionicons } from "@expo/vector-icons";
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
import React, { useEffect } from "react";
import { Platform, StyleSheet, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -156,7 +157,7 @@ const BottomSheetContent: React.FC<{
}));
return (
<View
<BottomSheetScrollView
className='px-4 pb-8 pt-2'
style={{
paddingLeft: Math.max(16, insets.left),
@@ -167,7 +168,7 @@ const BottomSheetContent: React.FC<{
{wrappedGroups.map((group, index) => (
<OptionGroupComponent key={index} group={group} />
))}
</View>
</BottomSheetScrollView>
);
};
@@ -228,25 +229,48 @@ const PlatformDropdownComponent = ({
const items = [];
// Add Picker for radio options if present
// Add Picker for radio options ONLY if there's a group title
// Otherwise render as individual buttons
if (radioOptions.length > 0) {
items.push(
<Picker
key={`picker-${groupIndex}`}
label={group.title || "Options"}
options={radioOptions.map((opt) => opt.label)}
variant='menu'
selectedIndex={radioOptions.findIndex(
(opt) => opt.selected,
)}
onOptionSelected={(event: any) => {
const index = event.nativeEvent.index;
const selectedOption = radioOptions[index];
selectedOption?.onPress();
onOptionSelect?.(selectedOption?.value);
}}
/>,
);
if (group.title) {
// Use Picker for grouped options
items.push(
<Picker
key={`picker-${groupIndex}`}
label={group.title}
options={radioOptions.map((opt) => opt.label)}
variant='menu'
selectedIndex={radioOptions.findIndex(
(opt) => opt.selected,
)}
onOptionSelected={(event: any) => {
const index = event.nativeEvent.index;
const selectedOption = radioOptions[index];
selectedOption?.onPress();
onOptionSelect?.(selectedOption?.value);
}}
/>,
);
} else {
// Render radio options as direct buttons
radioOptions.forEach((option, optionIndex) => {
items.push(
<Button
key={`radio-${groupIndex}-${optionIndex}`}
systemImage={
option.selected ? "checkmark.circle.fill" : "circle"
}
onPress={() => {
option.onPress();
onOptionSelect?.(option.value);
}}
disabled={option.disabled}
>
{option.label}
</Button>,
);
});
}
}
// Add Buttons for toggle options

View File

@@ -35,7 +35,6 @@ export const AppLanguageSelector: React.FC<Props> = () => {
return [
{
title: t("home.settings.languages.title"),
options,
},
];

View File

@@ -49,7 +49,6 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
return [
{
title: t("home.settings.audio.language"),
options,
},
];

View File

@@ -92,7 +92,6 @@ export const OtherSettings: React.FC = () => {
const orientationOptions = useMemo(
() => [
{
title: t("home.settings.other.orientation"),
options: orientations.map((orientation) => ({
type: "radio" as const,
label: t(ScreenOrientationEnum[orientation]),
@@ -109,7 +108,6 @@ export const OtherSettings: React.FC = () => {
const bitrateOptions = useMemo(
() => [
{
title: t("home.settings.other.default_quality"),
options: BITRATES.map((bitrate) => ({
type: "radio" as const,
label: bitrate.key,
@@ -125,7 +123,6 @@ export const OtherSettings: React.FC = () => {
const autoPlayEpisodeOptions = useMemo(
() => [
{
title: t("home.settings.other.max_auto_play_episode_count"),
options: AUTOPLAY_EPISODES_COUNT(t).map((item) => ({
type: "radio" as const,
label: item.key,

View File

@@ -65,7 +65,6 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
return [
{
title: t("home.settings.subtitles.language"),
options,
},
];
@@ -82,7 +81,6 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
return [
{
title: t("home.settings.subtitles.subtitle_mode"),
options,
},
];

View File

@@ -1,8 +1,10 @@
import { Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import { Platform, TouchableOpacity } from "react-native";
import { Text } from "@/components/common/Text";
import { FilterSheet } from "@/components/filters/FilterSheet";
import React, { useMemo } from "react";
import { Platform, View } from "react-native";
import {
type OptionGroup,
PlatformDropdown,
} from "@/components/PlatformDropdown";
import { useHaptic } from "@/hooks/useHaptic";
export type ScaleFactor =
@@ -94,56 +96,51 @@ export const ScaleFactorSelector: React.FC<ScaleFactorSelectorProps> = ({
disabled = false,
}) => {
const lightHapticFeedback = useHaptic("light");
const [open, setOpen] = useState(false);
// 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,
);
const optionGroups = useMemo<OptionGroup[]>(() => {
return [
{
options: SCALE_FACTOR_OPTIONS.map((option) => ({
type: "radio" as const,
label: option.label,
value: option.id,
selected: option.id === currentScale,
onPress: () => handleScaleSelect(option.id),
disabled,
})),
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentScale, disabled]);
return (
<>
<TouchableOpacity
disabled={disabled}
const trigger = useMemo(
() => (
<View
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
style={{ opacity: disabled ? 0.5 : 1 }}
onPress={() => setOpen(true)}
>
<Ionicons name='search-outline' size={24} color='white' />
</TouchableOpacity>
</View>
),
[disabled],
);
<FilterSheet
open={open}
setOpen={setOpen}
title='Scale Factor'
data={SCALE_FACTOR_OPTIONS}
values={currentOption ? [currentOption] : []}
multiple={false}
searchFilter={(item, query) => {
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 <Text>{option.label}</Text>;
}}
set={(vals) => {
const chosen = vals[0] as ScaleFactorOption | undefined;
if (chosen) {
handleScaleSelect(chosen.id);
}
}}
/>
</>
// Hide on TV platforms
if (Platform.isTV) return null;
return (
<PlatformDropdown
title='Scale Factor'
groups={optionGroups}
trigger={trigger}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
);
};

View File

@@ -1,8 +1,10 @@
import { Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import { Platform, TouchableOpacity } from "react-native";
import { Text } from "@/components/common/Text";
import { FilterSheet } from "@/components/filters/FilterSheet";
import React, { useMemo } from "react";
import { Platform, View } from "react-native";
import {
type OptionGroup,
PlatformDropdown,
} from "@/components/PlatformDropdown";
import { useHaptic } from "@/hooks/useHaptic";
export type AspectRatio = "default" | "16:9" | "4:3" | "1:1" | "21:9";
@@ -53,56 +55,51 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
disabled = false,
}) => {
const lightHapticFeedback = useHaptic("light");
const [open, setOpen] = useState(false);
// 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,
);
const optionGroups = useMemo<OptionGroup[]>(() => {
return [
{
options: ASPECT_RATIO_OPTIONS.map((option) => ({
type: "radio" as const,
label: option.label,
value: option.id,
selected: option.id === currentRatio,
onPress: () => handleRatioSelect(option.id),
disabled,
})),
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentRatio, disabled]);
return (
<>
<TouchableOpacity
disabled={disabled}
const trigger = useMemo(
() => (
<View
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
style={{ opacity: disabled ? 0.5 : 1 }}
onPress={() => setOpen(true)}
>
<Ionicons name='crop-outline' size={24} color='white' />
</TouchableOpacity>
</View>
),
[disabled],
);
<FilterSheet
open={open}
setOpen={setOpen}
title='Aspect Ratio'
data={ASPECT_RATIO_OPTIONS}
values={currentOption ? [currentOption] : []}
multiple={false}
searchFilter={(item, query) => {
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 <Text>{option.label}</Text>;
}}
set={(vals) => {
const chosen = vals[0] as AspectRatioOption | undefined;
if (chosen) {
handleRatioSelect(chosen.id);
}
}}
/>
</>
// Hide on TV platforms
if (Platform.isTV) return null;
return (
<PlatformDropdown
title='Aspect Ratio'
groups={optionGroups}
trigger={trigger}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
);
};

View File

@@ -1,7 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useMemo, useRef } from "react";
import { Platform, TouchableOpacity } from "react-native";
import { Platform, View } from "react-native";
import { BITRATES } from "@/components/BitrateSelector";
import {
type OptionGroup,
@@ -133,9 +133,9 @@ const DropdownView = () => {
// Memoize the trigger to prevent re-renders
const trigger = useMemo(
() => (
<TouchableOpacity className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'>
<View className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'>
<Ionicons name='ellipsis-horizontal' size={24} color={"white"} />
</TouchableOpacity>
</View>
),
[],
);

View File

@@ -13,7 +13,7 @@ export const useControlsTimeout = ({
isSliding,
episodeView,
onHideControls,
timeout = 4000,
timeout = 10000,
}: UseControlsTimeoutProps) => {
const controlsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);