mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
fix: modal for android + dropdown for ios
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,7 +35,6 @@ export const AppLanguageSelector: React.FC<Props> = () => {
|
||||
|
||||
return [
|
||||
{
|
||||
title: t("home.settings.languages.title"),
|
||||
options,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -49,7 +49,6 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
|
||||
|
||||
return [
|
||||
{
|
||||
title: t("home.settings.audio.language"),
|
||||
options,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const useControlsTimeout = ({
|
||||
isSliding,
|
||||
episodeView,
|
||||
onHideControls,
|
||||
timeout = 4000,
|
||||
timeout = 10000,
|
||||
}: UseControlsTimeoutProps) => {
|
||||
const controlsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user