chore: expo 55 upgrade

Upgraded expo from 54 to 55

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
Lance Chant
2026-05-27 09:33:55 +02:00
parent f1c2b9de53
commit 5609db73e5
8 changed files with 475 additions and 733 deletions

View File

@@ -7,8 +7,6 @@
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
"userInterfaceStyle": "dark",
"jsEngine": "hermes",
"newArchEnabled": true,
"assetBundlePatterns": ["**/*"],
"ios": {
"requireFullScreen": true,
@@ -38,7 +36,6 @@
"appleTeamId": "MWD5K362T8"
},
"android": {
"jsEngine": "hermes",
"versionCode": 92,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png",

View File

@@ -362,8 +362,8 @@ function Layout() {
url = `/(auth)/(tabs)/home/items/page?id=${itemId}`;
// summarized season notification for multiple episodes. Bring them to series season
} else {
const seriesId = data.seriesId;
const seasonIndex = data.seasonIndex;
const seriesId = data?.seriesId;
const seasonIndex = data?.seasonIndex;
if (seasonIndex) {
url = `/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`;
} else {

902
bun.lock

File diff suppressed because it is too large Load Diff

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 { 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";
@@ -255,22 +262,31 @@ const PlatformDropdownComponent = ({
if (radioOptions.length > 0) {
if (group.title) {
// Use Picker for grouped options
const selectedRadio = radioOptions.find(
(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={selectedRadio?.value}
onSelectionChange={(value) => {
const selectedOption = radioOptions.find(
(opt) => opt.value === value,
);
selectedOption?.onPress();
onOptionSelect?.(selectedOption?.value);
}}
/>,
>
{radioOptions.map((opt) => (
<SwiftUIText
key={String(opt.value)}
modifiers={[tag(opt.value)]}
>
{opt.label}
</SwiftUIText>
))}
</Picker>,
);
} else {
// Render radio options as direct buttons
@@ -281,13 +297,15 @@ const PlatformDropdownComponent = ({
systemImage={
option.selected ? "checkmark.circle.fill" : "circle"
}
modifiers={
option.disabled ? [disabled(true)] : undefined
}
onPress={() => {
option.onPress();
onOptionSelect?.(option.value);
}}
disabled={option.disabled}
>
{option.label}
<Text>{option.label}</Text>
</Button>,
);
});
@@ -302,13 +320,13 @@ const PlatformDropdownComponent = ({
systemImage={
option.value ? "checkmark.circle.fill" : "circle"
}
modifiers={option.disabled ? [disabled(true)] : undefined}
onPress={() => {
option.onToggle();
onOptionSelect?.(option.value);
}}
disabled={option.disabled}
>
{option.label}
<Text>{option.label}</Text>
</Button>,
);
});
@@ -318,12 +336,12 @@ const PlatformDropdownComponent = ({
items.push(
<Button
key={`action-${groupIndex}-${optionIndex}`}
modifiers={option.disabled ? [disabled(true)] : undefined}
onPress={() => {
option.onPress();
}}
disabled={option.disabled}
>
{option.label}
<Text>{option.label}</Text>
</Button>,
);
});

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

@@ -1,7 +1,10 @@
import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import type { Audio as AudioType } from "expo-av";
import { Audio } from "expo-av";
import {
type AudioPlayer,
createAudioPlayer,
setAudioModeAsync,
} from "expo-audio";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { Platform } from "react-native";
@@ -18,7 +21,7 @@ const FADE_STEP_MS = 50;
* Returns a cleanup function that cancels the fade.
*/
function fadeVolume(
sound: AudioType.Sound,
player: AudioPlayer,
from: number,
to: number,
duration: number,
@@ -38,23 +41,19 @@ function fadeVolume(
const tick = () => {
if (cancelled || step >= steps) {
if (!cancelled) {
sound.setVolumeAsync(to).catch(() => {});
player.volume = to;
}
resolve();
return;
}
step++;
current += delta;
sound
.setVolumeAsync(Math.max(0, Math.min(1, current)))
.catch(() => {})
.then(() => {
if (!cancelled) {
setTimeout(tick, FADE_STEP_MS);
} else {
resolve();
}
});
player.volume = Math.max(0, Math.min(1, current));
if (!cancelled) {
setTimeout(tick, FADE_STEP_MS);
} else {
resolve();
}
};
tick();
@@ -64,41 +63,35 @@ function fadeVolume(
}
// --- Module-level singleton state ---
let sharedSound: AudioType.Sound | null = null;
let sharedPlayer: AudioPlayer | null = null;
let currentSongId: string | null = null;
let ownerCount = 0;
let activeFade: { cancel: () => void } | null = null;
let cleanupPromise: Promise<void> | null = null;
/** Fade out, stop, and unload the shared sound. */
async function teardownSharedSound(): Promise<void> {
const sound = sharedSound;
if (!sound) return;
/** Fade out, stop, and release the shared player. */
async function teardownSharedPlayer(): Promise<void> {
const player = sharedPlayer;
if (!player) return;
activeFade?.cancel();
activeFade = null;
try {
const status = await sound.getStatusAsync();
if (status.isLoaded) {
const currentVolume = status.volume ?? TARGET_VOLUME;
const fade = fadeVolume(sound, currentVolume, 0, FADE_OUT_DURATION);
if (player.isLoaded) {
const currentVolume = player.volume ?? TARGET_VOLUME;
const fade = fadeVolume(player, currentVolume, 0, FADE_OUT_DURATION);
activeFade = fade;
await fade.promise;
activeFade = null;
await sound.stopAsync();
await sound.unloadAsync();
player.pause();
}
} catch {
try {
await sound.unloadAsync();
} catch {
// ignore
}
// ignore
}
if (sharedSound === sound) {
sharedSound = null;
if (sharedPlayer === player) {
sharedPlayer = null;
currentSongId = null;
}
}
@@ -106,7 +99,7 @@ async function teardownSharedSound(): Promise<void> {
/** Begin cleanup idempotently; returns the shared promise. */
function beginCleanup(): Promise<void> {
if (!cleanupPromise) {
cleanupPromise = teardownSharedSound().finally(() => {
cleanupPromise = teardownSharedPlayer().finally(() => {
cleanupPromise = null;
});
}
@@ -154,12 +147,12 @@ export function useTVThemeMusic(itemId: string | undefined) {
const startPlayback = async () => {
// If the same song is already playing, keep it going
if (currentSongId === songId && sharedSound) {
if (currentSongId === songId && sharedPlayer) {
return;
}
// If a different song is playing (or cleanup is in progress), tear it down first
if (sharedSound || cleanupPromise) {
if (sharedPlayer || cleanupPromise) {
activeFade?.cancel();
activeFade = null;
await beginCleanup();
@@ -167,14 +160,14 @@ export function useTVThemeMusic(itemId: string | undefined) {
if (!mounted) return;
const sound = new Audio.Sound();
sharedSound = sound;
const player = createAudioPlayer(null);
sharedPlayer = player;
currentSongId = songId;
try {
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true,
staysActiveInBackground: false,
await setAudioModeAsync({
playsInSilentMode: true,
shouldPlayInBackground: false,
});
const params = new URLSearchParams({
@@ -190,19 +183,19 @@ export function useTVThemeMusic(itemId: string | undefined) {
EnableRemoteMedia: "false",
});
const url = `${api.basePath}/Audio/${themeItem.Id}/universal?${params.toString()}`;
await sound.loadAsync({ uri: url });
player.replace({ uri: url });
if (!mounted || sharedSound !== sound) {
await sound.unloadAsync();
if (!mounted || sharedPlayer !== player) {
player.pause();
return;
}
await sound.setIsLoopingAsync(true);
await sound.setVolumeAsync(0);
await sound.playAsync();
player.loop = true;
player.volume = 0;
player.play();
if (mounted && sharedSound === sound) {
const fade = fadeVolume(sound, 0, TARGET_VOLUME, FADE_IN_DURATION);
if (mounted && sharedPlayer === player) {
const fade = fadeVolume(player, 0, TARGET_VOLUME, FADE_IN_DURATION);
activeFade = fade;
await fade.promise;
activeFade = null;

View File

@@ -27,60 +27,61 @@
"dependencies": {
"@bottom-tabs/react-navigation": "1.1.0",
"@douglowder/expo-av-route-picker-view": "^0.0.5",
"@expo/metro-runtime": "~6.1.1",
"@expo/metro-runtime": "~55.0.11",
"@expo/react-native-action-sheet": "^4.1.1",
"@expo/ui": "0.2.0-beta.9",
"@expo/ui": "~55.0.17",
"@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "5.2.8",
"@jellyfin/sdk": "^0.13.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-community/netinfo": "11.5.2",
"@react-navigation/material-top-tabs": "7.4.9",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native": "^7.2.5",
"@react-navigation/native-stack": "~7.14.5",
"@shopify/flash-list": "2.0.2",
"@tanstack/query-sync-storage-persister": "^5.90.18",
"@tanstack/react-pacer": "^0.19.1",
"@tanstack/react-query": "5.90.17",
"@tanstack/react-query-persist-client": "^5.90.18",
"axios": "^1.7.9",
"expo": "~54.0.31",
"expo-application": "~7.0.8",
"expo-asset": "~12.0.12",
"expo-av": "^16.0.8",
"expo-background-task": "~1.0.10",
"expo-blur": "~15.0.8",
"expo-brightness": "~14.0.8",
"expo-build-properties": "~1.0.10",
"expo-camera": "^55.0.18",
"expo-constants": "18.0.13",
"expo-crypto": "^15.0.8",
"expo-dev-client": "~6.0.20",
"expo-device": "~8.0.10",
"expo-font": "~14.0.10",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linear-gradient": "~15.0.8",
"expo-linking": "~8.0.11",
"expo-localization": "~17.0.8",
"expo-location": "^19.0.8",
"expo-notifications": "~0.32.16",
"expo-router": "~6.0.21",
"expo-screen-orientation": "~9.0.8",
"expo-secure-store": "^15.0.8",
"expo-sharing": "~14.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-system-ui": "~6.0.9",
"expo-task-manager": "14.0.9",
"expo-web-browser": "~15.0.10",
"expo": "~55.0.26",
"expo-application": "~55.0.15",
"expo-asset": "~55.0.17",
"expo-audio": "~55.0.0",
"expo-background-task": "~55.0.18",
"expo-blur": "~55.0.14",
"expo-brightness": "~55.0.13",
"expo-build-properties": "~55.0.14",
"expo-camera": "~55.0.19",
"expo-constants": "~55.0.16",
"expo-crypto": "~55.0.15",
"expo-dev-client": "~55.0.35",
"expo-device": "~55.0.17",
"expo-font": "~55.0.8",
"expo-haptics": "~55.0.14",
"expo-image": "~55.0.11",
"expo-linear-gradient": "~55.0.14",
"expo-linking": "~55.0.15",
"expo-localization": "~55.0.15",
"expo-location": "~55.1.10",
"expo-notifications": "~55.0.23",
"expo-router": "~55.0.16",
"expo-screen-orientation": "~55.0.16",
"expo-secure-store": "~55.0.14",
"expo-sharing": "~55.0.20",
"expo-splash-screen": "~55.0.21",
"expo-status-bar": "~55.0.6",
"expo-system-ui": "~55.0.18",
"expo-task-manager": "~55.0.16",
"expo-web-browser": "~55.0.16",
"i18next": "^25.0.0",
"jotai": "2.16.2",
"lodash": "4.17.21",
"nativewind": "^2.0.11",
"patch-package": "^8.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-i18next": "16.5.3",
"react-native": "npm:react-native-tvos@0.81.5-2",
"react-native": "npm:react-native-tvos@0.83.6-0",
"react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "1.1.0",
"react-native-circular-progress": "^1.4.1",
@@ -89,7 +90,7 @@
"react-native-device-info": "^15.0.0",
"react-native-draggable-flatlist": "^4.0.3",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "2.28.0",
"react-native-gesture-handler": "~2.30.0",
"react-native-glass-effect-view": "^1.0.0",
"react-native-google-cast": "^4.9.1",
"react-native-image-colors": "^2.4.0",
@@ -97,13 +98,13 @@
"react-native-ios-utilities": "5.2.0",
"react-native-mmkv": "4.1.1",
"react-native-nitro-modules": "0.33.1",
"react-native-pager-view": "^6.9.1",
"react-native-pager-view": "8.0.0",
"react-native-qrcode-svg": "^6.3.21",
"react-native-reanimated": "~4.1.1",
"react-native-reanimated": "4.2.1",
"react-native-reanimated-carousel": "4.0.3",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.18.0",
"react-native-svg": "15.12.1",
"react-native-svg": "15.15.3",
"react-native-text-ticker": "^1.15.0",
"react-native-track-player": "github:lovegaoshi/react-native-track-player#APM",
"react-native-udp": "^4.1.7",
@@ -111,7 +112,7 @@
"react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"react-native-worklets": "0.7.4",
"sonner-native": "0.21.2",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
@@ -124,7 +125,7 @@
"@react-native-tvos/config-tv": "0.1.4",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.23",
"@types/react": "19.1.17",
"@types/react": "~19.2.10",
"@types/react-test-renderer": "19.1.0",
"cross-env": "10.1.0",
"expo-doctor": "1.17.14",
@@ -165,8 +166,5 @@
"patchedDependencies": {
"react-native-screens@4.18.0": "bun-patches/react-native-screens@4.18.0.patch",
"react-native-udp@4.1.7": "bun-patches/react-native-udp@4.1.7.patch"
},
"resolutions": {
"expo-constants": "18.0.13"
}
}