chore: expo 55 upgrade (#1594)

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
This commit is contained in:
lance chant
2026-05-27 21:38:08 +02:00
committed by GitHub
parent 82eaf62354
commit eb02ac253a
14 changed files with 747 additions and 788 deletions

View File

@@ -1,8 +1,21 @@
import { Button, ContextMenu, Host, Picker } from "@expo/ui/swift-ui";
import {
Button,
Host,
Menu,
Picker,
Text as SwiftUIText,
} from "@expo/ui/swift-ui";
import { disabled, tag } from "@expo/ui/swift-ui/modifiers";
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 React, { useEffect, useState } from "react";
import {
type LayoutChangeEvent,
Platform,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { useGlobalModal } from "@/providers/GlobalModalProvider";
@@ -201,6 +214,24 @@ const PlatformDropdownComponent = ({
}: PlatformDropdownProps) => {
const { showModal, hideModal, isVisible } = useGlobalModal();
// @expo/ui's <Host> (SDK 55) fills its available space by default, and
// `matchContents` doesn't help here: it reports the native Menu's size via
// setStyleSize and overrides any explicit size. Instead we measure the
// trigger's intrinsic size in plain RN (off-layout) and pin it on the Host.
const [triggerSize, setTriggerSize] = useState<{
width: number;
height: number;
} | null>(null);
const handleMeasureTrigger = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout;
setTriggerSize((prev) =>
prev && prev.width === width && prev.height === height
? prev
: { width, height },
);
};
// Handle controlled open state for Android
useEffect(() => {
if (Platform.OS === "android" && controlledOpen === true) {
@@ -232,10 +263,24 @@ const PlatformDropdownComponent = ({
if (Platform.OS === "ios") {
return (
<Host style={expoUIConfig?.hostStyle}>
<ContextMenu>
<ContextMenu.Trigger>{trigger}</ContextMenu.Trigger>
<ContextMenu.Items>
<View>
{/* Hidden measurer: lays the trigger out normally to capture its
intrinsic size, which we then pin onto the Host below. */}
<View style={StyleSheet.absoluteFill} pointerEvents='none' aria-hidden>
<View
style={{ alignSelf: "flex-start" }}
onLayout={handleMeasureTrigger}
>
{trigger}
</View>
</View>
<Host
style={[
triggerSize ?? { opacity: 0 },
expoUIConfig?.hostStyle as any,
]}
>
<Menu label={trigger}>
{groups.flatMap((group, groupIndex) => {
// Check if this group has radio options
const radioOptions = group.options.filter(
@@ -254,23 +299,37 @@ const PlatformDropdownComponent = ({
// Otherwise render as individual buttons
if (radioOptions.length > 0) {
if (group.title) {
// Use Picker for grouped options
// Use Picker for grouped options.
// Use the option index (a stable primitive) as the
// tag/selection value and React key. Option `value`s can be
// objects (e.g. bitrate / media source), which collapse to
// "[object Object]" as a key and never match the Picker's
// primitive selection.
const selectedRadioIndex = radioOptions.findIndex(
(opt) => opt.selected,
);
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];
selection={
selectedRadioIndex >= 0 ? selectedRadioIndex : undefined
}
onSelectionChange={(index) => {
const selectedOption = radioOptions[index as number];
selectedOption?.onPress();
onOptionSelect?.(selectedOption?.value);
}}
/>,
>
{radioOptions.map((opt, optionIndex) => (
<SwiftUIText
key={`radio-${groupIndex}-${optionIndex}`}
modifiers={[tag(optionIndex)]}
>
{opt.label}
</SwiftUIText>
))}
</Picker>,
);
} else {
// Render radio options as direct buttons
@@ -278,17 +337,18 @@ const PlatformDropdownComponent = ({
items.push(
<Button
key={`radio-${groupIndex}-${optionIndex}`}
label={option.label}
systemImage={
option.selected ? "checkmark.circle.fill" : "circle"
}
modifiers={
option.disabled ? [disabled(true)] : undefined
}
onPress={() => {
option.onPress();
onOptionSelect?.(option.value);
}}
disabled={option.disabled}
>
{option.label}
</Button>,
/>,
);
});
}
@@ -299,17 +359,16 @@ const PlatformDropdownComponent = ({
items.push(
<Button
key={`toggle-${groupIndex}-${optionIndex}`}
label={option.label}
systemImage={
option.value ? "checkmark.circle.fill" : "circle"
}
modifiers={option.disabled ? [disabled(true)] : undefined}
onPress={() => {
option.onToggle();
onOptionSelect?.(option.value);
}}
disabled={option.disabled}
>
{option.label}
</Button>,
/>,
);
});
@@ -318,21 +377,20 @@ const PlatformDropdownComponent = ({
items.push(
<Button
key={`action-${groupIndex}-${optionIndex}`}
label={option.label}
modifiers={option.disabled ? [disabled(true)] : undefined}
onPress={() => {
option.onPress();
}}
disabled={option.disabled}
>
{option.label}
</Button>,
/>,
);
});
return items;
})}
</ContextMenu.Items>
</ContextMenu>
</Host>
</Menu>
</Host>
</View>
);
}

View File

@@ -1,4 +1,11 @@
import { Button, ContextMenu, Host, Picker } from "@expo/ui/swift-ui";
import {
Button,
ContextMenu,
Host,
Picker,
Text as SwiftUIText,
} from "@expo/ui/swift-ui";
import { buttonStyle, tag } from "@expo/ui/swift-ui/modifiers";
import { Platform, View } from "react-native";
import { FilterButton } from "@/components/filters/FilterButton";
import { JellyseerrSearchSort } from "@/components/jellyseerr/JellyseerrIndexPage";
@@ -43,38 +50,37 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
<ContextMenu>
<ContextMenu.Trigger>
<Button
variant='glass'
modifiers={[]}
modifiers={[buttonStyle("glass")]}
systemImage='line.3.horizontal.decrease.circle'
></Button>
</ContextMenu.Trigger>
<ContextMenu.Items>
<Picker
label={t("library.filters.sort_by")}
options={sortOptions.map((item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`),
)}
variant='menu'
selectedIndex={sortOptions.indexOf(
jellyseerrOrderBy as unknown as string,
)}
onOptionSelected={(event: any) => {
const index = event.nativeEvent.index;
setJellyseerrOrderBy(
sortOptions[index] as unknown as JellyseerrSearchSort,
);
selection={jellyseerrOrderBy as unknown as string}
onSelectionChange={(value) => {
setJellyseerrOrderBy(value as unknown as JellyseerrSearchSort);
}}
/>
>
{sortOptions.map((item) => (
<SwiftUIText key={item} modifiers={[tag(item)]}>
{t(`home.settings.plugins.jellyseerr.order_by.${item}`)}
</SwiftUIText>
))}
</Picker>
<Picker
label={t("library.filters.sort_order")}
options={orderOptions.map((item) => t(`library.filters.${item}`))}
variant='menu'
selectedIndex={orderOptions.indexOf(jellyseerrSortOrder)}
onOptionSelected={(event: any) => {
const index = event.nativeEvent.index;
setJellyseerrSortOrder(orderOptions[index]);
selection={jellyseerrSortOrder}
onSelectionChange={(value) => {
setJellyseerrSortOrder(value as "asc" | "desc");
}}
/>
>
{orderOptions.map((item) => (
<SwiftUIText key={item} modifiers={[tag(item)]}>
{t(`library.filters.${item}`)}
</SwiftUIText>
))}
</Picker>
</ContextMenu.Items>
</ContextMenu>
</Host>

View File

@@ -1,5 +1,7 @@
import { Button, Host } from "@expo/ui/swift-ui";
import { buttonStyle } from "@expo/ui/swift-ui/modifiers";
import { Platform, TouchableOpacity, View } from "react-native";
import { Text } from "@/components/common/Text";
import { Tag } from "@/components/GenreTags";
type SearchType = "Library" | "Discover";
@@ -28,10 +30,14 @@ export const SearchTabButtons: React.FC<SearchTabButtonsProps> = ({
}}
>
<Button
variant={searchType === "Library" ? "glassProminent" : "glass"}
modifiers={[
buttonStyle(
searchType === "Library" ? "glassProminent" : "glass",
),
]}
onPress={() => setSearchType("Library")}
>
{t("search.library")}
<Text>{t("search.library")}</Text>
</Button>
</Host>
<Host
@@ -44,10 +50,14 @@ export const SearchTabButtons: React.FC<SearchTabButtonsProps> = ({
}}
>
<Button
variant={searchType === "Discover" ? "glassProminent" : "glass"}
modifiers={[
buttonStyle(
searchType === "Discover" ? "glassProminent" : "glass",
),
]}
onPress={() => setSearchType("Discover")}
>
{t("search.discover")}
<Text>{t("search.discover")}</Text>
</Button>
</Host>
</>

View File

@@ -123,7 +123,9 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
</View>
<View className='flex flex-row items-center space-x-2'>
{!Platform.isTV && (
{/* Rotate toggle is Android-only: iOS does not reliably rotate the
player back to portrait programmatically. */}
{Platform.OS === "android" && (
<TouchableOpacity
onPress={toggleOrientation}
disabled={isTogglingOrientation}