diff --git a/.gitignore b/.gitignore
index 7c36d0f9..2813f770 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,4 +64,5 @@ credentials.json
streamyfin-4fec1-firebase-adminsdk.json
# Version and Backup Files
-/version-backup-*
\ No newline at end of file
+/version-backup-*
+modules/background-downloader/android/build/*
diff --git a/GLOBAL_MODAL_GUIDE.md b/GLOBAL_MODAL_GUIDE.md
new file mode 100644
index 00000000..5426ca72
--- /dev/null
+++ b/GLOBAL_MODAL_GUIDE.md
@@ -0,0 +1,232 @@
+# Global Modal System with Gorhom Bottom Sheet
+
+This guide explains how to use the global modal system implemented in this project.
+
+## Overview
+
+The global modal system allows you to trigger a bottom sheet modal from anywhere in your app programmatically, and render any component inside it.
+
+## Architecture
+
+The system consists of three main parts:
+
+1. **GlobalModalProvider** (`providers/GlobalModalProvider.tsx`) - Context provider that manages modal state
+2. **GlobalModal** (`components/GlobalModal.tsx`) - The actual modal component rendered at root level
+3. **useGlobalModal** hook - Hook to interact with the modal from anywhere
+
+## Setup (Already Configured)
+
+The system is already integrated into your app:
+
+```tsx
+// In app/_layout.tsx
+
+
+ {/* Your app content */}
+
+
+
+```
+
+## Usage
+
+### Basic Usage
+
+```tsx
+import { useGlobalModal } from "@/providers/GlobalModalProvider";
+import { View, Text } from "react-native";
+
+function MyComponent() {
+ const { showModal, hideModal } = useGlobalModal();
+
+ const handleOpenModal = () => {
+ showModal(
+
+ Hello from Modal!
+
+ );
+ };
+
+ return (
+
+ );
+}
+```
+
+### With Custom Options
+
+```tsx
+const handleOpenModal = () => {
+ showModal(
+ ,
+ {
+ snapPoints: ["25%", "50%", "90%"], // Custom snap points
+ enablePanDownToClose: true, // Allow swipe to close
+ backgroundStyle: { // Custom background
+ backgroundColor: "#000000",
+ },
+ }
+ );
+};
+```
+
+### Programmatic Control
+
+```tsx
+// Open modal
+showModal();
+
+// Close modal from within the modal content
+function ModalContent() {
+ const { hideModal } = useGlobalModal();
+
+ return (
+
+
+
+ );
+}
+
+// Close modal from outside
+hideModal();
+```
+
+### In Event Handlers or Functions
+
+```tsx
+function useApiCall() {
+ const { showModal } = useGlobalModal();
+
+ const fetchData = async () => {
+ try {
+ const result = await api.fetch();
+
+ // Show success modal
+ showModal(
+
+ );
+ } catch (error) {
+ // Show error modal
+ showModal(
+
+ );
+ }
+ };
+
+ return fetchData;
+}
+```
+
+## API Reference
+
+### `useGlobalModal()`
+
+Returns an object with the following properties:
+
+- **`showModal(content, options?)`** - Show the modal with given content
+ - `content: ReactNode` - Any React component or element to render
+ - `options?: ModalOptions` - Optional configuration object
+
+- **`hideModal()`** - Programmatically hide the modal
+
+- **`isVisible: boolean`** - Current visibility state of the modal
+
+### `ModalOptions`
+
+```typescript
+interface ModalOptions {
+ enableDynamicSizing?: boolean; // Auto-size based on content (default: true)
+ snapPoints?: (string | number)[]; // Fixed snap points (e.g., ["50%", "90%"])
+ enablePanDownToClose?: boolean; // Allow swipe down to close (default: true)
+ backgroundStyle?: object; // Custom background styles
+ handleIndicatorStyle?: object; // Custom handle indicator styles
+}
+```
+
+## Examples
+
+See `components/ExampleGlobalModalUsage.tsx` for comprehensive examples including:
+- Simple content modal
+- Modal with custom snap points
+- Complex component in modal
+- Success/error modals triggered from functions
+
+## Default Styling
+
+The modal uses these default styles (can be overridden via options):
+
+```typescript
+{
+ enableDynamicSizing: true,
+ enablePanDownToClose: true,
+ backgroundStyle: {
+ backgroundColor: "#171717", // Dark background
+ },
+ handleIndicatorStyle: {
+ backgroundColor: "white",
+ },
+}
+```
+
+## Best Practices
+
+1. **Keep content in separate components** - Don't inline large JSX in `showModal()` calls
+2. **Use the hook in custom hooks** - Create specialized hooks like `useShowSuccessModal()` for reusable modal patterns
+3. **Handle cleanup** - The modal automatically clears content when closed
+4. **Avoid nesting** - Don't show modals from within modals
+5. **Consider UX** - Only use for important, contextual information that requires user attention
+
+## Using with PlatformDropdown
+
+When using `PlatformDropdown` with option groups, avoid setting a `title` on the `OptionGroup` if you're already passing a `title` prop to `PlatformDropdown`. This prevents nested menu behavior on iOS where users have to click through an extra layer.
+
+```tsx
+// Good - No title in option group (title is on PlatformDropdown)
+const optionGroups: OptionGroup[] = [
+ {
+ options: items.map((item) => ({
+ type: "radio",
+ label: item.name,
+ value: item,
+ selected: item.id === selected?.id,
+ onPress: () => onChange(item),
+ })),
+ },
+];
+
+
+
+// Bad - Causes nested menu on iOS
+const optionGroups: OptionGroup[] = [
+ {
+ title: "Items", // This creates a nested Picker on iOS
+ options: items.map((item) => ({
+ type: "radio",
+ label: item.name,
+ value: item,
+ selected: item.id === selected?.id,
+ onPress: () => onChange(item),
+ })),
+ },
+];
+```
+
+## Troubleshooting
+
+### Modal doesn't appear
+- Ensure `GlobalModalProvider` is above the component calling `useGlobalModal()`
+- Check that `BottomSheetModalProvider` is present in the tree
+- Verify `GlobalModal` component is rendered
+
+### Content is cut off
+- Use `enableDynamicSizing: true` for auto-sizing
+- Or specify appropriate `snapPoints`
+
+### Modal won't close
+- Ensure `enablePanDownToClose` is `true`
+- Check that backdrop is clickable
+- Use `hideModal()` for programmatic closing
diff --git a/app.config.js b/app.config.js
index 20860647..2e37927b 100644
--- a/app.config.js
+++ b/app.config.js
@@ -6,9 +6,6 @@ module.exports = ({ config }) => {
"react-native-google-cast",
{ useDefaultExpandedMediaControls: true },
]);
-
- // Add the background downloader plugin only for non-TV builds
- config.plugins.push("./plugins/withRNBackgroundDownloader.js");
}
// Only override googleServicesFile if env var is set
diff --git a/app.json b/app.json
index 0ffe5213..ab7b20e1 100644
--- a/app.json
+++ b/app.json
@@ -2,12 +2,13 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
- "version": "0.40.4",
+ "version": "0.46.2",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
"userInterfaceStyle": "dark",
"jsEngine": "hermes",
+ "newArchEnabled": true,
"assetBundlePatterns": ["**/*"],
"ios": {
"requireFullScreen": true,
@@ -37,7 +38,7 @@
},
"android": {
"jsEngine": "hermes",
- "versionCode": 76,
+ "versionCode": 81,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png",
@@ -77,6 +78,7 @@
"useFrameworks": "static"
},
"android": {
+ "buildArchs": ["arm64-v8a", "x86_64"],
"compileSdkVersion": 35,
"targetSdkVersion": 35,
"buildToolsVersion": "35.0.0",
@@ -115,10 +117,6 @@
}
}
],
- ["./plugins/withChangeNativeAndroidTextToWhite.js"],
- ["./plugins/withAndroidManifest.js"],
- ["./plugins/withTrustLocalCerts.js"],
- ["./plugins/withGradleProperties.js"],
[
"expo-splash-screen",
{
@@ -137,8 +135,12 @@
}
}
],
- "./plugins/with-runtime-framework-headers.js",
- "react-native-bottom-tabs"
+ "expo-web-browser",
+ ["./plugins/with-runtime-framework-headers.js"],
+ ["./plugins/withChangeNativeAndroidTextToWhite.js"],
+ ["./plugins/withAndroidManifest.js"],
+ ["./plugins/withTrustLocalCerts.js"],
+ ["./plugins/withGradleProperties.js"]
],
"experiments": {
"typedRoutes": true
@@ -157,7 +159,6 @@
},
"updates": {
"url": "https://u.expo.dev/e79219d1-797f-4fbe-9fa1-cfd360690a68"
- },
- "newArchEnabled": false
+ }
}
}
diff --git a/app/(auth)/(tabs)/(custom-links)/_layout.tsx b/app/(auth)/(tabs)/(custom-links)/_layout.tsx
index f3b436a7..3b8a58e2 100644
--- a/app/(auth)/(tabs)/(custom-links)/_layout.tsx
+++ b/app/(auth)/(tabs)/(custom-links)/_layout.tsx
@@ -9,7 +9,7 @@ export default function CustomMenuLayout() {
-
+
diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx
index f5eb2c5e..60d8749b 100644
--- a/app/(auth)/(tabs)/(home)/_layout.tsx
+++ b/app/(auth)/(tabs)/(home)/_layout.tsx
@@ -1,15 +1,8 @@
import { Feather, Ionicons } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
-import { useState } from "react";
import { useTranslation } from "react-i18next";
-import {
- ActivityIndicator,
- Platform,
- TouchableOpacity,
- View,
-} from "react-native";
+import { Platform, TouchableOpacity, View } from "react-native";
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
-import { eventBus } from "@/utils/eventBus";
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
@@ -37,7 +30,6 @@ export default function IndexLayout() {
{!Platform.isTV && (
<>
-
{user?.Policy?.IsAdministrator && }
>
@@ -49,49 +41,119 @@ export default function IndexLayout() {
(
+ _router.back()}
+ className='pl-0.5'
+ style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
+ >
+
+
+ ),
}}
/>
(
+ _router.back()}
+ className='pl-0.5'
+ style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
+ >
+
+
+ ),
}}
/>
(
+ _router.back()}
+ className='pl-0.5'
+ style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
+ >
+
+
+ ),
}}
/>
(
+ _router.back()}
+ className='pl-0.5'
+ style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
+ >
+
+
+ ),
}}
/>
(
+ _router.back()} className='pl-0.5'>
+
+
+ ),
}}
/>
(
+ _router.back()} className='pl-0.5'>
+
+
+ ),
}}
/>
(
+ _router.back()} className='pl-0.5'>
+
+
+ ),
}}
/>
(
+ _router.back()} className='pl-0.5'>
+
+
+ ),
}}
/>
(
+ _router.back()} className='pl-0.5'>
+
+
+ ),
presentation: "modal",
}}
/>
@@ -109,6 +176,11 @@ export default function IndexLayout() {
name='collections/[collectionId]'
options={{
title: "",
+ headerLeft: () => (
+ _router.back()} className='pl-0.5'>
+
+
+ ),
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
@@ -133,32 +205,6 @@ const SettingsButton = () => {
);
};
-const RefreshButton = () => {
- const [isRefreshing, setIsRefreshing] = useState(false);
-
- const handleRefresh = () => {
- setIsRefreshing(true);
- eventBus.emit("refreshHome");
- setTimeout(() => {
- setIsRefreshing(false);
- }, 2000);
- };
-
- return (
-
- {isRefreshing ? (
-
- ) : (
-
- )}
-
- );
-};
-
const SessionsButton = () => {
const router = useRouter();
const { sessions = [] } = useSessions({} as useSessionsProps);
diff --git a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx
index 794e3005..14ec23ec 100644
--- a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx
+++ b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx
@@ -1,8 +1,9 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { FlashList } from "@shopify/flash-list";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useCallback, useEffect, useMemo, useState } from "react";
-import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
+import { Alert, Platform, TouchableOpacity, View } from "react-native";
import { Text } from "@/components/common/Text";
import { EpisodeCard } from "@/components/downloads/EpisodeCard";
import {
@@ -23,21 +24,22 @@ export default function page() {
const [seasonIndexState, setSeasonIndexState] = useState(
{},
);
- const { getDownloadedItems, deleteItems } = useDownload();
+ const { downloadedItems, deleteItems } = useDownload();
const series = useMemo(() => {
try {
return (
- getDownloadedItems()
+ downloadedItems
?.filter((f) => f.item.SeriesId === seriesId)
?.sort(
- (a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber!,
+ (a, b) =>
+ (a.item.ParentIndexNumber ?? 0) - (b.item.ParentIndexNumber ?? 0),
) || []
);
} catch {
return [];
}
- }, [getDownloadedItems]);
+ }, [downloadedItems, seriesId]);
// Group episodes by season in a single pass
const seasonGroups = useMemo(() => {
@@ -91,7 +93,7 @@ export default function page() {
title: series[0].item.SeriesName,
});
} else {
- storage.delete(seriesId);
+ storage.remove(seriesId);
router.back();
}
}, [series]);
@@ -107,17 +109,22 @@ export default function page() {
},
{
text: "Delete",
- onPress: () => deleteItems(groupBySeason),
+ onPress: () =>
+ deleteItems(
+ groupBySeason
+ .map((item) => item.Id)
+ .filter((id) => id !== undefined),
+ ),
style: "destructive",
},
],
);
- }, [groupBySeason]);
+ }, [groupBySeason, deleteItems]);
return (
-
+
{series.length > 0 && (
-
+
)}
-
- {groupBySeason.map((episode, index) => (
-
- ))}
-
+ }
+ keyExtractor={(item, index) => item.Id ?? `episode-${index}`}
+ contentContainerStyle={{ paddingHorizontal: 16 }}
+ />
);
}
diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx
index 07d3dbc5..c71bebd8 100644
--- a/app/(auth)/(tabs)/(home)/downloads/index.tsx
+++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx
@@ -1,17 +1,17 @@
-import { Ionicons } from "@expo/vector-icons";
-import {
- BottomSheetBackdrop,
- type BottomSheetBackdropProps,
- BottomSheetModal,
- BottomSheetView,
-} from "@gorhom/bottom-sheet";
+import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { useNavigation, useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
-import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
+import {
+ Alert,
+ Platform,
+ ScrollView,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
-import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import ActiveDownloads from "@/components/downloads/ActiveDownloads";
@@ -26,18 +26,15 @@ import { writeToLog } from "@/utils/log";
export default function page() {
const navigation = useNavigation();
const { t } = useTranslation();
- const [queue, setQueue] = useAtom(queueAtom);
- const {
- removeProcess,
- getDownloadedItems,
- deleteFileByType,
- deleteAllFiles,
- } = useDownload();
+ const [_queue, _setQueue] = useAtom(queueAtom);
+ const { downloadedItems, deleteFileByType, deleteAllFiles } = useDownload();
const router = useRouter();
const bottomSheetModalRef = useRef(null);
const [showMigration, setShowMigration] = useState(false);
+ const _insets = useSafeAreaInsets();
+
const migration_20241124 = () => {
Alert.alert(
t("home.downloads.new_app_version_requires_re_download"),
@@ -62,7 +59,7 @@ export default function page() {
);
};
- const downloadedFiles = getDownloadedItems();
+ const downloadedFiles = useMemo(() => downloadedItems, [downloadedItems]);
const movies = useMemo(() => {
try {
@@ -106,7 +103,10 @@ export default function page() {
useEffect(() => {
navigation.setOptions({
headerRight: () => (
-
+
f.item) || []} />
),
@@ -119,7 +119,7 @@ export default function page() {
}
}, [showMigration]);
- const deleteMovies = () =>
+ const _deleteMovies = () =>
deleteFileByType("Movie")
.then(() =>
toast.success(
@@ -130,7 +130,7 @@ export default function page() {
writeToLog("ERROR", reason);
toast.error(t("home.downloads.toasts.failed_to_delete_all_movies"));
});
- const deleteShows = () =>
+ const _deleteShows = () =>
deleteFileByType("Episode")
.then(() =>
toast.success(
@@ -141,38 +141,39 @@ export default function page() {
writeToLog("ERROR", reason);
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
});
- const deleteOtherMedia = () =>
+ const _deleteOtherMedia = () =>
Promise.all(
- otherMedia.map((item) =>
- deleteFileByType(item.item.Type)
- .then(() =>
- toast.success(
- t("home.downloads.toasts.deleted_media_successfully", {
- type: item.item.Type,
- }),
- ),
- )
- .catch((reason) => {
- writeToLog("ERROR", reason);
- toast.error(
- t("home.downloads.toasts.failed_to_delete_media", {
- type: item.item.Type,
- }),
- );
- }),
- ),
+ otherMedia
+ .filter((item) => item.item.Type)
+ .map((item) =>
+ deleteFileByType(item.item.Type!)
+ .then(() =>
+ toast.success(
+ t("home.downloads.toasts.deleted_media_successfully", {
+ type: item.item.Type,
+ }),
+ ),
+ )
+ .catch((reason) => {
+ writeToLog("ERROR", reason);
+ toast.error(
+ t("home.downloads.toasts.failed_to_delete_media", {
+ type: item.item.Type,
+ }),
+ );
+ }),
+ ),
);
- const deleteAllMedia = async () =>
- await Promise.all([deleteMovies(), deleteShows(), deleteOtherMedia()]);
-
return (
- <>
-
-
-
-
-
+
+
+
+ {/* Queue card - hidden */}
+ {/*
{t("home.downloads.queue")}
@@ -214,139 +215,96 @@ export default function page() {
{t("home.downloads.no_items_in_queue")}
)}
-
+ */}
-
+
+
+
+ {movies.length > 0 && (
+
+
+
+ {t("home.downloads.movies")}
+
+
+ {movies?.length}
+
-
- {movies.length > 0 && (
-
-
-
- {t("home.downloads.movies")}
-
-
- {movies?.length}
-
-
-
-
- {movies?.map((item) => (
-
-
-
- ))}
-
-
+
+
+ {movies?.map((item) => (
+
+
+
+ ))}
- )}
- {groupedBySeries.length > 0 && (
-
-
-
- {t("home.downloads.tvseries")}
-
-
-
- {groupedBySeries?.length}
-
-
-
-
-
- {groupedBySeries?.map((items) => (
-
- i.item)}
- key={items[0].item.SeriesId}
- />
-
- ))}
-
-
-
- )}
-
- {otherMedia.length > 0 && (
-
-
-
- {t("home.downloads.other_media")}
-
-
-
- {otherMedia?.length}
-
-
-
-
-
- {otherMedia?.map((item) => (
-
-
-
- ))}
-
-
-
- )}
- {downloadedFiles?.length === 0 && (
-
-
- {t("home.downloads.no_downloaded_items")}
+
+
+ )}
+ {groupedBySeries.length > 0 && (
+
+
+
+ {t("home.downloads.tvseries")}
+
+
+
+ {groupedBySeries?.length}
- )}
+
+
+
+ {groupedBySeries?.map((items) => (
+
+ i.item)}
+ key={items[0].item.SeriesId}
+ />
+
+ ))}
+
+
-
-
- (
-
)}
- >
-
-
-
-
- {otherMedia.length > 0 && (
-
- )}
-
+
+ {otherMedia.length > 0 && (
+
+
+
+ {t("home.downloads.other_media")}
+
+
+ {otherMedia?.length}
+
+
+
+
+ {otherMedia?.map((item) => (
+
+
+
+ ))}
+
+
-
-
- >
+ )}
+ {downloadedFiles?.length === 0 && (
+
+
+ {t("home.downloads.no_downloaded_items")}
+
+
+ )}
+
+
);
}
diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx
index dc04e43b..ad951c36 100644
--- a/app/(auth)/(tabs)/(home)/index.tsx
+++ b/app/(auth)/(tabs)/(home)/index.tsx
@@ -1,5 +1,16 @@
-import { HomeIndex } from "@/components/settings/HomeIndex";
+import { useSettings } from "@/utils/atoms/settings";
+import { Home } from "../../../../components/home/Home";
+import { HomeWithCarousel } from "../../../../components/home/HomeWithCarousel";
-export default function page() {
- return ;
-}
+const Index = () => {
+ const { settings } = useSettings();
+ const showLargeHomeCarousel = settings.showLargeHomeCarousel ?? false;
+
+ if (showLargeHomeCarousel) {
+ return ;
+ }
+
+ return ;
+};
+
+export default Index;
diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx
index cd5e32a0..0ed8fc94 100644
--- a/app/(auth)/(tabs)/(home)/sessions/index.tsx
+++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx
@@ -1,11 +1,9 @@
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
-import {
- HardwareAccelerationType,
- type SessionInfoDto,
-} from "@jellyfin/sdk/lib/generated-client";
+import { HardwareAccelerationType } from "@jellyfin/sdk/lib/generated-client";
import {
GeneralCommandType,
PlaystateCommand,
+ SessionInfoDto,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import { FlashList } from "@shopify/flash-list";
@@ -13,7 +11,7 @@ import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
-import { TouchableOpacity, View } from "react-native";
+import { Platform, TouchableOpacity, View } from "react-native";
import { Badge } from "@/components/Badge";
import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
@@ -49,14 +47,13 @@ export default function page() {
}
keyExtractor={(item) => item.Id || ""}
- estimatedItemSize={200}
/>
);
}
diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx
index 91f569df..ae1d00ee 100644
--- a/app/(auth)/(tabs)/(home)/settings.tsx
+++ b/app/(auth)/(tabs)/(home)/settings.tsx
@@ -46,7 +46,7 @@ export default function settings() {
logout();
}}
>
-
+
{t("home.settings.log_out_button")}
@@ -56,12 +56,16 @@ export default function settings() {
return (
-
+
@@ -115,6 +119,7 @@ export default function settings() {
{!Platform.isTV && }
+
);
diff --git a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx
index 7af8fc03..d2b9b852 100644
--- a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx
@@ -1,4 +1,3 @@
-import * as FileSystem from "expo-file-system";
import { useNavigation } from "expo-router";
import * as Sharing from "expo-sharing";
import { useCallback, useEffect, useId, useMemo, useState } from "react";
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx
index 798fa2cc..1723fe4b 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx
@@ -16,6 +16,7 @@ import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
@@ -204,154 +205,154 @@ const page: React.FC = () => {
const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []);
+ const _insets = useSafeAreaInsets();
+
const ListHeaderComponent = useCallback(
() => (
-
- ,
- },
- {
- key: "genre",
- component: (
- {
- if (!api) return null;
- const response = await getFilterApi(
- api,
- ).getQueryFiltersLegacy({
- userId: user?.Id,
- parentId: collectionId,
- });
- return response.data.Genres || [];
- }}
- set={setSelectedGenres}
- values={selectedGenres}
- title={t("library.filters.genres")}
- renderItemLabel={(item) => item.toString()}
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- {
- key: "year",
- component: (
- {
- if (!api) return null;
- const response = await getFilterApi(
- api,
- ).getQueryFiltersLegacy({
- userId: user?.Id,
- parentId: collectionId,
- });
- return response.data.Years || [];
- }}
- set={setSelectedYears}
- values={selectedYears}
- title={t("library.filters.years")}
- renderItemLabel={(item) => item.toString()}
- searchFilter={(item, search) => item.includes(search)}
- />
- ),
- },
- {
- key: "tags",
- component: (
- {
- if (!api) return null;
- const response = await getFilterApi(
- api,
- ).getQueryFiltersLegacy({
- userId: user?.Id,
- parentId: collectionId,
- });
- return response.data.Tags || [];
- }}
- set={setSelectedTags}
- values={selectedTags}
- title={t("library.filters.tags")}
- renderItemLabel={(item) => item.toString()}
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- {
- key: "sortBy",
- component: (
- sortOptions.map((s) => s.key)}
- set={setSortBy}
- values={sortBy}
- title={t("library.filters.sort_by")}
- renderItemLabel={(item) =>
- sortOptions.find((i) => i.key === item)?.value || ""
- }
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- {
- key: "sortOrder",
- component: (
- sortOrderOptions.map((s) => s.key)}
- set={setSortOrder}
- values={sortOrder}
- title={t("library.filters.sort_order")}
- renderItemLabel={(item) =>
- sortOrderOptions.find((i) => i.key === item)?.value || ""
- }
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- ]}
- renderItem={({ item }) => item.component}
- keyExtractor={(item) => item.key}
- />
-
+ ,
+ },
+ {
+ key: "genre",
+ component: (
+ {
+ if (!api) return null;
+ const response = await getFilterApi(
+ api,
+ ).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: collectionId,
+ });
+ return response.data.Genres || [];
+ }}
+ set={setSelectedGenres}
+ values={selectedGenres}
+ title={t("library.filters.genres")}
+ renderItemLabel={(item) => item.toString()}
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ {
+ key: "year",
+ component: (
+ {
+ if (!api) return null;
+ const response = await getFilterApi(
+ api,
+ ).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: collectionId,
+ });
+ return response.data.Years || [];
+ }}
+ set={setSelectedYears}
+ values={selectedYears}
+ title={t("library.filters.years")}
+ renderItemLabel={(item) => item.toString()}
+ searchFilter={(item, search) => item.includes(search)}
+ />
+ ),
+ },
+ {
+ key: "tags",
+ component: (
+ {
+ if (!api) return null;
+ const response = await getFilterApi(
+ api,
+ ).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: collectionId,
+ });
+ return response.data.Tags || [];
+ }}
+ set={setSelectedTags}
+ values={selectedTags}
+ title={t("library.filters.tags")}
+ renderItemLabel={(item) => item.toString()}
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ {
+ key: "sortBy",
+ component: (
+ sortOptions.map((s) => s.key)}
+ set={setSortBy}
+ values={sortBy}
+ title={t("library.filters.sort_by")}
+ renderItemLabel={(item) =>
+ sortOptions.find((i) => i.key === item)?.value || ""
+ }
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ {
+ key: "sortOrder",
+ component: (
+ sortOrderOptions.map((s) => s.key)}
+ set={setSortOrder}
+ values={sortOrder}
+ title={t("library.filters.sort_order")}
+ renderItemLabel={(item) =>
+ sortOrderOptions.find((i) => i.key === item)?.value || ""
+ }
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ ]}
+ renderItem={({ item }) => item.component}
+ keyExtractor={(item) => item.key}
+ />
),
[
collectionId,
@@ -393,7 +394,6 @@ const page: React.FC = () => {
data={flatData}
renderItem={renderItem}
keyExtractor={keyExtractor}
- estimatedItemSize={255}
numColumns={
orientation === ScreenOrientation.Orientation.PORTRAIT_UP ? 3 : 5
}
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
index 12e1c82e..28533cd0 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
@@ -19,31 +19,29 @@ import { Text } from "@/components/common/Text";
import { GenreTags } from "@/components/GenreTags";
import Cast from "@/components/jellyseerr/Cast";
import DetailFacts from "@/components/jellyseerr/DetailFacts";
+import RequestModal from "@/components/jellyseerr/RequestModal";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
+import { PlatformDropdown } from "@/components/PlatformDropdown";
import { JellyserrRatings } from "@/components/Ratings";
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
import { ItemActions } from "@/components/series/SeriesActions";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
+import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import {
type IssueType,
IssueTypeName,
} from "@/utils/jellyseerr/server/constants/issue";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
+import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
+import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
-import RequestModal from "@/components/jellyseerr/RequestModal";
-import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
-import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
-import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
-
const Page: React.FC = () => {
const insets = useSafeAreaInsets();
const params = useLocalSearchParams();
@@ -65,6 +63,7 @@ const Page: React.FC = () => {
const [issueType, setIssueType] = useState();
const [issueMessage, setIssueMessage] = useState();
const [requestBody, _setRequestBody] = useState();
+ const [issueTypeDropdownOpen, setIssueTypeDropdownOpen] = useState(false);
const advancedReqModalRef = useRef(null);
const bottomSheetModalRef = useRef(null);
@@ -115,6 +114,10 @@ const Page: React.FC = () => {
}
}, [jellyseerrApi, details, result, issueType, issueMessage]);
+ const handleIssueModalDismiss = useCallback(() => {
+ setIssueTypeDropdownOpen(false);
+ }, []);
+
const setRequestBody = useCallback(
(body: MediaRequestBody) => {
_setRequestBody(body);
@@ -156,11 +159,31 @@ const Page: React.FC = () => {
[details],
);
+ const issueTypeOptionGroups = useMemo(
+ () => [
+ {
+ title: t("jellyseerr.types"),
+ options: Object.entries(IssueTypeName)
+ .reverse()
+ .map(([key, value]) => ({
+ type: "radio" as const,
+ label: value,
+ value: key,
+ selected: key === String(issueType),
+ onPress: () => setIssueType(key as unknown as IssueType),
+ })),
+ },
+ ],
+ [issueType, t],
+ );
+
useEffect(() => {
if (details) {
navigation.setOptions({
headerRight: () => (
-
+
),
@@ -355,6 +378,8 @@ const Page: React.FC = () => {
backgroundColor: "#171717",
}}
backdropComponent={renderBackdrop}
+ stackBehavior='push'
+ onDismiss={handleIssueModalDismiss}
>
@@ -364,50 +389,25 @@ const Page: React.FC = () => {
-
-
-
-
-
- {t("jellyseerr.issue_type")}
+
+
+ {t("jellyseerr.issue_type")}
+
+
+
+ {issueType
+ ? IssueTypeName[issueType]
+ : t("jellyseerr.select_an_issue")}
-
-
- {issueType
- ? IssueTypeName[issueType]
- : t("jellyseerr.select_an_issue")}
-
-
-
-
-
- {t("jellyseerr.types")}
-
- {Object.entries(IssueTypeName)
- .reverse()
- .map(([key, value], _idx) => (
-
- setIssueType(key as unknown as IssueType)
- }
- >
-
- {value}
-
-
- ))}
-
-
+ }
+ title={t("jellyseerr.types")}
+ open={issueTypeDropdownOpen}
+ onOpenChange={setIssueTypeDropdownOpen}
+ />
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx
index 78cad4a3..2636d5c0 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx
@@ -65,9 +65,11 @@ const page: React.FC = () => {
const { data: allEpisodes, isLoading } = useQuery({
queryKey: ["AllEpisodes", item?.Id],
queryFn: async () => {
- const res = await getTvShowsApi(api!).getEpisodes({
- seriesId: item?.Id!,
- userId: user?.Id!,
+ if (!api || !user?.Id || !item?.Id) return [];
+
+ const res = await getTvShowsApi(api).getEpisodes({
+ seriesId: item.Id,
+ userId: user.Id,
enableUserData: true,
// Note: Including trick play is necessary to enable trick play downloads
fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
index d795fb91..481881fc 100644
--- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
+++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
@@ -271,145 +271,143 @@ const Page = () => {
const ListHeaderComponent = useCallback(
() => (
-
- ,
- },
- {
- key: "genre",
- component: (
- {
- if (!api) return null;
- const response = await getFilterApi(
- api,
- ).getQueryFiltersLegacy({
- userId: user?.Id,
- parentId: libraryId,
- });
- return response.data.Genres || [];
- }}
- set={setSelectedGenres}
- values={selectedGenres}
- title={t("library.filters.genres")}
- renderItemLabel={(item) => item.toString()}
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- {
- key: "year",
- component: (
- {
- if (!api) return null;
- const response = await getFilterApi(
- api,
- ).getQueryFiltersLegacy({
- userId: user?.Id,
- parentId: libraryId,
- });
- return response.data.Years || [];
- }}
- set={setSelectedYears}
- values={selectedYears}
- title={t("library.filters.years")}
- renderItemLabel={(item) => item.toString()}
- searchFilter={(item, search) => item.includes(search)}
- />
- ),
- },
- {
- key: "tags",
- component: (
- {
- if (!api) return null;
- const response = await getFilterApi(
- api,
- ).getQueryFiltersLegacy({
- userId: user?.Id,
- parentId: libraryId,
- });
- return response.data.Tags || [];
- }}
- set={setSelectedTags}
- values={selectedTags}
- title={t("library.filters.tags")}
- renderItemLabel={(item) => item.toString()}
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- {
- key: "sortBy",
- component: (
- sortOptions.map((s) => s.key)}
- set={setSortBy}
- values={sortBy}
- title={t("library.filters.sort_by")}
- renderItemLabel={(item) =>
- sortOptions.find((i) => i.key === item)?.value || ""
- }
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- {
- key: "sortOrder",
- component: (
- sortOrderOptions.map((s) => s.key)}
- set={setSortOrder}
- values={sortOrder}
- title={t("library.filters.sort_order")}
- renderItemLabel={(item) =>
- sortOrderOptions.find((i) => i.key === item)?.value || ""
- }
- searchFilter={(item, search) =>
- item.toLowerCase().includes(search.toLowerCase())
- }
- />
- ),
- },
- ]}
- renderItem={({ item }) => item.component}
- keyExtractor={(item) => item.key}
- />
-
+ ,
+ },
+ {
+ key: "genre",
+ component: (
+ {
+ if (!api) return null;
+ const response = await getFilterApi(
+ api,
+ ).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: libraryId,
+ });
+ return response.data.Genres || [];
+ }}
+ set={setSelectedGenres}
+ values={selectedGenres}
+ title={t("library.filters.genres")}
+ renderItemLabel={(item) => item.toString()}
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ {
+ key: "year",
+ component: (
+ {
+ if (!api) return null;
+ const response = await getFilterApi(
+ api,
+ ).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: libraryId,
+ });
+ return response.data.Years || [];
+ }}
+ set={setSelectedYears}
+ values={selectedYears}
+ title={t("library.filters.years")}
+ renderItemLabel={(item) => item.toString()}
+ searchFilter={(item, search) => item.includes(search)}
+ />
+ ),
+ },
+ {
+ key: "tags",
+ component: (
+ {
+ if (!api) return null;
+ const response = await getFilterApi(
+ api,
+ ).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: libraryId,
+ });
+ return response.data.Tags || [];
+ }}
+ set={setSelectedTags}
+ values={selectedTags}
+ title={t("library.filters.tags")}
+ renderItemLabel={(item) => item.toString()}
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ {
+ key: "sortBy",
+ component: (
+ sortOptions.map((s) => s.key)}
+ set={setSortBy}
+ values={sortBy}
+ title={t("library.filters.sort_by")}
+ renderItemLabel={(item) =>
+ sortOptions.find((i) => i.key === item)?.value || ""
+ }
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ {
+ key: "sortOrder",
+ component: (
+ sortOrderOptions.map((s) => s.key)}
+ set={setSortOrder}
+ values={sortOrder}
+ title={t("library.filters.sort_order")}
+ renderItemLabel={(item) =>
+ sortOrderOptions.find((i) => i.key === item)?.value || ""
+ }
+ searchFilter={(item, search) =>
+ item.toLowerCase().includes(search.toLowerCase())
+ }
+ />
+ ),
+ },
+ ]}
+ renderItem={({ item }) => item.component}
+ keyExtractor={(item) => item.key}
+ />
),
[
libraryId,
@@ -453,7 +451,6 @@ const Page = () => {
renderItem={renderItem}
extraData={[orientation, nrOfCols]}
keyExtractor={keyExtractor}
- estimatedItemSize={244}
numColumns={nrOfCols}
onEndReached={() => {
if (hasNextPage) {
diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx
index 8c7fb259..ebf77b66 100644
--- a/app/(auth)/(tabs)/(libraries)/_layout.tsx
+++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx
@@ -1,85 +1,166 @@
import { Ionicons } from "@expo/vector-icons";
import { Stack } from "expo-router";
-import { useState } from "react";
import { useTranslation } from "react-i18next";
-import { Platform, TouchableOpacity } from "react-native";
-import { LibraryOptionsSheet } from "@/components/settings/LibraryOptionsSheet";
+import { Platform, View } from "react-native";
+import { PlatformDropdown } from "@/components/PlatformDropdown";
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { useSettings } from "@/utils/atoms/settings";
export default function IndexLayout() {
const { settings, updateSettings, pluginSettings } = useSettings();
- const [optionsSheetOpen, setOptionsSheetOpen] = useState(false);
const { t } = useTranslation();
if (!settings?.libraryOptions) return null;
return (
- <>
-
-
- !pluginSettings?.libraryOptions?.locked &&
- !Platform.isTV && (
- setOptionsSheetOpen(true)}
- className='flex flex-row items-center justify-center w-9 h-9'
- >
-
-
- ),
- }}
- />
-
- {Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
-
- ))}
-
-
-
- updateSettings({
- libraryOptions: {
- ...settings.libraryOptions,
- ...options,
- },
- })
- }
- disabled={pluginSettings?.libraryOptions?.locked}
+
+
+ !pluginSettings?.libraryOptions?.locked &&
+ !Platform.isTV && (
+
+
+
+ }
+ title={t("library.options.display")}
+ groups={[
+ {
+ title: t("library.options.display"),
+ options: [
+ {
+ type: "radio",
+ label: t("library.options.row"),
+ value: "row",
+ selected: settings.libraryOptions.display === "row",
+ onPress: () =>
+ updateSettings({
+ libraryOptions: {
+ ...settings.libraryOptions,
+ display: "row",
+ },
+ }),
+ },
+ {
+ type: "radio",
+ label: t("library.options.list"),
+ value: "list",
+ selected: settings.libraryOptions.display === "list",
+ onPress: () =>
+ updateSettings({
+ libraryOptions: {
+ ...settings.libraryOptions,
+ display: "list",
+ },
+ }),
+ },
+ ],
+ },
+ {
+ title: t("library.options.image_style"),
+ options: [
+ {
+ type: "radio",
+ label: t("library.options.poster"),
+ value: "poster",
+ selected:
+ settings.libraryOptions.imageStyle === "poster",
+ onPress: () =>
+ updateSettings({
+ libraryOptions: {
+ ...settings.libraryOptions,
+ imageStyle: "poster",
+ },
+ }),
+ },
+ {
+ type: "radio",
+ label: t("library.options.cover"),
+ value: "cover",
+ selected:
+ settings.libraryOptions.imageStyle === "cover",
+ onPress: () =>
+ updateSettings({
+ libraryOptions: {
+ ...settings.libraryOptions,
+ imageStyle: "cover",
+ },
+ }),
+ },
+ ],
+ },
+ {
+ title: "Options",
+ options: [
+ {
+ type: "toggle",
+ label: t("library.options.show_titles"),
+ value: settings.libraryOptions.showTitles,
+ onToggle: () =>
+ updateSettings({
+ libraryOptions: {
+ ...settings.libraryOptions,
+ showTitles: !settings.libraryOptions.showTitles,
+ },
+ }),
+ disabled:
+ settings.libraryOptions.imageStyle === "poster",
+ },
+ {
+ type: "toggle",
+ label: t("library.options.show_stats"),
+ value: settings.libraryOptions.showStats,
+ onToggle: () =>
+ updateSettings({
+ libraryOptions: {
+ ...settings.libraryOptions,
+ showStats: !settings.libraryOptions.showStats,
+ },
+ }),
+ },
+ ],
+ },
+ ]}
+ />
+ ),
+ }}
/>
- >
+
+ {Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
+
+ ))}
+
+
);
}
diff --git a/app/(auth)/(tabs)/(libraries)/index.tsx b/app/(auth)/(tabs)/(libraries)/index.tsx
index c638e5d2..b56397f9 100644
--- a/app/(auth)/(tabs)/(libraries)/index.tsx
+++ b/app/(auth)/(tabs)/(libraries)/index.tsx
@@ -7,7 +7,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
-import { StyleSheet, View } from "react-native";
+import { Platform, StyleSheet, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
@@ -84,11 +84,11 @@ export default function index() {
extraData={settings}
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
- paddingTop: 17,
+ paddingTop: Platform.OS === "android" ? 17 : 0,
paddingHorizontal: settings?.libraryOptions?.display === "row" ? 0 : 17,
paddingBottom: 150,
- paddingLeft: insets.left,
- paddingRight: insets.right,
+ paddingLeft: insets.left + 17,
+ paddingRight: insets.right + 17,
}}
data={libraries}
renderItem={({ item }) => }
@@ -105,7 +105,6 @@ export default function index() {
)
}
- estimatedItemSize={200}
/>
);
}
diff --git a/app/(auth)/(tabs)/(search)/_layout.tsx b/app/(auth)/(tabs)/(search)/_layout.tsx
index 4577a38a..f4d34978 100644
--- a/app/(auth)/(tabs)/(search)/_layout.tsx
+++ b/app/(auth)/(tabs)/(search)/_layout.tsx
@@ -28,7 +28,7 @@ export default function SearchLayout() {
options={{
title: "",
headerShown: !Platform.isTV,
- headerBlurEffect: "prominent",
+ headerBlurEffect: "none",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx
index 5a95ad48..bedf5ffa 100644
--- a/app/(auth)/(tabs)/(search)/index.tsx
+++ b/app/(auth)/(tabs)/(search)/index.tsx
@@ -24,8 +24,6 @@ import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
-import { FilterButton } from "@/components/filters/FilterButton";
-import { Tag } from "@/components/GenreTags";
import { ItemCardText } from "@/components/ItemCardText";
import {
JellyseerrSearchSort,
@@ -33,8 +31,10 @@ import {
} from "@/components/jellyseerr/JellyseerrIndexPage";
import MoviePoster from "@/components/posters/MoviePoster";
import SeriesPoster from "@/components/posters/SeriesPoster";
+import { DiscoverFilters } from "@/components/search/DiscoverFilters";
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
+import { SearchTabButtons } from "@/components/search/SearchTabButtons";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
@@ -284,67 +284,30 @@ export default function search() {
)}
{jellyseerrApi && (
-
- setSearchType("Library")}>
-
-
- setSearchType("Discover")}>
-
-
+
+
{searchType === "Discover" &&
!loading &&
noResults &&
debouncedSearch.length > 0 && (
-
-
- Object.keys(JellyseerrSearchSort).filter((v) =>
- Number.isNaN(Number(v)),
- )
- }
- set={(value) => setJellyseerrOrderBy(value[0])}
- values={[jellyseerrOrderBy]}
- title={t("library.filters.sort_by")}
- renderItemLabel={(item) =>
- t(`home.settings.plugins.jellyseerr.order_by.${item}`)
- }
- disableSearch={true}
- />
- ["asc", "desc"]}
- set={(value) => setJellyseerrSortOrder(value[0])}
- values={[jellyseerrSortOrder]}
- title={t("library.filters.sort_order")}
- renderItemLabel={(item) => t(`library.filters.${item}`)}
- disableSearch={true}
- />
-
+
)}
-
+
)}
diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx
index 41a76eb6..e8a79728 100644
--- a/app/(auth)/(tabs)/_layout.tsx
+++ b/app/(auth)/(tabs)/_layout.tsx
@@ -55,6 +55,7 @@ export default function TabLayout() {
backgroundColor: "#121212",
}}
tabBarActiveTintColor={Colors.primary}
+ activeIndicatorColor={"#392c3b"}
scrollEdgeAppearance='default'
>
@@ -70,10 +71,7 @@ export default function TabLayout() {
tabBarIcon:
Platform.OS === "android"
? (_e) => require("@/assets/icons/house.fill.png")
- : ({ focused }) =>
- focused
- ? { sfSymbol: "house.fill" }
- : { sfSymbol: "house" },
+ : (_e) => ({ sfSymbol: "house.fill" }),
}}
/>
require("@/assets/icons/magnifyingglass.png")
- : ({ focused }) =>
- focused
- ? { sfSymbol: "magnifyingglass" }
- : { sfSymbol: "magnifyingglass" },
+ : (_e) => ({ sfSymbol: "magnifyingglass" }),
}}
/>
- focused
- ? require("@/assets/icons/heart.fill.png")
- : require("@/assets/icons/heart.png")
- : ({ focused }) =>
- focused
- ? { sfSymbol: "heart.fill" }
- : { sfSymbol: "heart" },
+ ? (_e) => require("@/assets/icons/heart.fill.png")
+ : (_e) => ({ sfSymbol: "heart.fill" }),
}}
/>
require("@/assets/icons/server.rack.png")
- : ({ focused }) =>
- focused
- ? { sfSymbol: "rectangle.stack.fill" }
- : { sfSymbol: "rectangle.stack" },
+ : (_e) => ({ sfSymbol: "rectangle.stack.fill" }),
}}
/>
require("@/assets/icons/list.png")
- : ({ focused }) =>
- focused
- ? { sfSymbol: "list.dash.fill" }
- : { sfSymbol: "list.dash" },
+ : (_e) => ({ sfSymbol: "list.dash.fill" }),
}}
/>
diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx
index 587368bd..537c11a4 100644
--- a/app/(auth)/player/direct-player.tsx
+++ b/app/(auth)/player/direct-player.tsx
@@ -29,6 +29,7 @@ import {
VLCColor,
} from "@/constants/SubtitleConstants";
import { useHaptic } from "@/hooks/useHaptic";
+import { useOrientation } from "@/hooks/useOrientation";
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import { useWebSocket } from "@/hooks/useWebsockets";
@@ -76,7 +77,10 @@ export default function page() {
: require("react-native-volume-manager");
const downloadUtils = useDownload();
- const downloadedFiles = downloadUtils.getDownloadedItems();
+ const downloadedFiles = useMemo(
+ () => downloadUtils.getDownloadedItems(),
+ [downloadUtils.getDownloadedItems],
+ );
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
@@ -106,6 +110,7 @@ export default function page() {
playbackPosition?: string;
}>();
const { settings } = useSettings();
+ const { lockOrientation, unlockOrientation } = useOrientation();
const offline = offlineStr === "true";
const playbackManager = usePlaybackManager();
@@ -168,6 +173,16 @@ export default function page() {
}
}, [itemId, offline, api, user?.Id]);
+ useEffect(() => {
+ if (settings?.defaultVideoOrientation) {
+ lockOrientation(settings.defaultVideoOrientation);
+ }
+
+ return () => {
+ unlockOrientation();
+ };
+ }, [settings?.defaultVideoOrientation]);
+
interface Stream {
mediaSource: MediaSourceInfo;
sessionId: string;
@@ -283,12 +298,14 @@ export default function page() {
};
const reportPlaybackStopped = useCallback(async () => {
+ if (!item?.Id || !stream?.sessionId) return;
+
const currentTimeInTicks = msToTicks(progress.get());
await getPlaystateApi(api!).onPlaybackStopped({
- itemId: item?.Id!,
+ itemId: item.Id,
mediaSourceId: mediaSourceId,
positionTicks: currentTimeInTicks,
- playSessionId: stream?.sessionId!,
+ playSessionId: stream.sessionId,
});
}, [
api,
@@ -319,9 +336,9 @@ export default function page() {
}, [navigation, stop]);
const currentPlayStateInfo = useCallback(() => {
- if (!stream) return;
+ if (!stream || !item?.Id) return;
return {
- itemId: item?.Id!,
+ itemId: item.Id,
audioStreamIndex: audioIndex ? audioIndex : undefined,
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
mediaSourceId: mediaSourceId,
@@ -765,6 +782,7 @@ export default function page() {
setAspectRatio={setAspectRatio}
setScaleFactor={setScaleFactor}
isVlc
+ api={api}
downloadedFiles={downloadedFiles}
/>
)}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 34302de4..1a04ae9b 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,18 +1,23 @@
import "@/augmentations";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
+import { DarkTheme, ThemeProvider } from "@react-navigation/native";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import * as BackgroundTask from "expo-background-task";
+import * as Device from "expo-device";
import { Platform } from "react-native";
+import { GlobalModal } from "@/components/GlobalModal";
import i18n from "@/i18n";
import { DownloadProvider } from "@/providers/DownloadProvider";
+import { GlobalModalProvider } from "@/providers/GlobalModalProvider";
import {
apiAtom,
getOrSetDeviceId,
- getTokenFromStorage,
JellyfinProvider,
} from "@/providers/JellyfinProvider";
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
import { WebSocketProvider } from "@/providers/WebSocketProvider";
-import { type Settings, useSettings } from "@/utils/atoms/settings";
+import { useSettings } from "@/utils/atoms/settings";
import {
BACKGROUND_FETCH_TASK,
BACKGROUND_FETCH_TASK_SESSIONS,
@@ -26,44 +31,30 @@ import {
} from "@/utils/log";
import { storage } from "@/utils/mmkv";
-const BackGroundDownloader = !Platform.isTV
- ? require("@kesha-antonov/react-native-background-downloader")
- : null;
-
-import { DarkTheme, ThemeProvider } from "@react-navigation/native";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-
-import * as BackgroundTask from "expo-background-task";
-
-import * as Device from "expo-device";
-import * as FileSystem from "expo-file-system";
-
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
-import { getLocales } from "expo-localization";
-import { router, Stack, useSegments } from "expo-router";
-import * as SplashScreen from "expo-splash-screen";
-
-import * as TaskManager from "expo-task-manager";
-import { Provider as JotaiProvider } from "jotai";
-import { useEffect, useRef, useState } from "react";
-import { I18nextProvider } from "react-i18next";
-import { Appearance, AppState } from "react-native";
-import { SystemBars } from "react-native-edge-to-edge";
-import { GestureHandlerRootView } from "react-native-gesture-handler";
-import * as ScreenOrientation from "@/packages/expo-screen-orientation";
-import "react-native-reanimated";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
+import { getLocales } from "expo-localization";
import type { EventSubscription } from "expo-modules-core";
+import { getDevicePushTokenAsync } from "expo-notifications";
import type {
Notification,
NotificationResponse,
} from "expo-notifications/build/Notifications.types";
-import type { ExpoPushToken } from "expo-notifications/build/Tokens.types";
-import { useAtom } from "jotai";
-import { Toaster } from "sonner-native";
+import type { DevicePushToken } from "expo-notifications/build/Tokens.types";
+import { router, Stack, useSegments } from "expo-router";
+import * as SplashScreen from "expo-splash-screen";
+import * as TaskManager from "expo-task-manager";
+import { Provider as JotaiProvider, useAtom } from "jotai";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { I18nextProvider } from "react-i18next";
+import { Appearance } from "react-native";
+import { SystemBars } from "react-native-edge-to-edge";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
import { userAtom } from "@/providers/JellyfinProvider";
import { store } from "@/utils/store";
+import "react-native-reanimated";
+import { Toaster } from "sonner-native";
if (!Platform.isTV) {
Notifications.setNotificationHandler({
@@ -131,24 +122,7 @@ if (!Platform.isTV) {
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
console.log("TaskManager ~ trigger");
-
- const settingsData = storage.getString("settings");
-
- if (!settingsData) return BackgroundTask.BackgroundTaskResult.Failed;
-
- const settings: Partial = JSON.parse(settingsData);
-
- if (!settings?.autoDownload)
- return BackgroundTask.BackgroundTaskResult.Failed;
-
- const token = getTokenFromStorage();
- const deviceId = getOrSetDeviceId();
- const baseDirectory = FileSystem.documentDirectory;
-
- if (!token || !deviceId || !baseDirectory)
- return BackgroundTask.BackgroundTaskResult.Failed;
-
- // Be sure to return the successful result type!
+ // Background fetch task placeholder - currently unused
return BackgroundTask.BackgroundTaskResult.Success;
});
}
@@ -213,11 +187,7 @@ export default function RootLayout() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
- staleTime: 0,
- refetchOnMount: true,
- refetchOnReconnect: true,
- refetchOnWindowFocus: true,
- retryOnMount: true,
+ staleTime: 30000,
},
},
});
@@ -226,8 +196,7 @@ function Layout() {
const { settings } = useSettings();
const [user] = useAtom(userAtom);
const [api] = useAtom(apiAtom);
- const appState = useRef(AppState.currentState);
- const segments = useSegments();
+ const _segments = useSegments();
useEffect(() => {
i18n.changeLanguage(
@@ -237,26 +206,26 @@ function Layout() {
useNotificationObserver();
- const [expoPushToken, setExpoPushToken] = useState();
+ const [pushToken, setPushToken] = useState();
const notificationListener = useRef(null);
const responseListener = useRef(null);
useEffect(() => {
- if (!Platform.isTV && expoPushToken && api && user) {
+ if (!Platform.isTV && pushToken && api && user) {
api
?.post("/Streamyfin/device", {
- token: expoPushToken.data,
+ token: pushToken.data,
deviceId: getOrSetDeviceId(),
userId: user.Id,
})
- .then((_) => console.log("Posted expo push token"))
+ .then((_) => console.log("Posted device push token"))
.catch((_) =>
- writeErrorLog("Failed to push expo push token to plugin"),
+ writeErrorLog("Failed to push device push token to plugin"),
);
} else console.log("No token available");
- }, [api, expoPushToken, user]);
+ }, [api, pushToken, user]);
- async function registerNotifications() {
+ const registerNotifications = useCallback(async () => {
if (Platform.OS === "android") {
console.log("Setting android notification channel 'default'");
await Notifications?.setNotificationChannelAsync("default", {
@@ -287,13 +256,11 @@ function Layout() {
// only create push token for real devices (pointless for emulators)
if (Device.isDevice) {
- Notifications?.getExpoPushTokenAsync({
- projectId: "streamyfin-4fec1",
- })
- .then((token: ExpoPushToken) => token && setExpoPushToken(token))
+ getDevicePushTokenAsync()
+ .then((token: DevicePushToken) => token && setPushToken(token))
.catch((reason: any) => console.log("Failed to get token", reason));
}
- }
+ }, [user]);
useEffect(() => {
if (!Platform.isTV) {
@@ -357,61 +324,7 @@ function Layout() {
responseListener.current?.remove();
};
}
- }, [user, api]);
-
- useEffect(() => {
- if (Platform.isTV) {
- return;
- }
-
- if (segments.includes("direct-player" as never)) {
- if (
- !settings.followDeviceOrientation &&
- settings.defaultVideoOrientation
- ) {
- ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
- }
- return;
- }
-
- if (settings.followDeviceOrientation === true) {
- ScreenOrientation.unlockAsync();
- } else {
- ScreenOrientation.lockAsync(
- ScreenOrientation.OrientationLock.PORTRAIT_UP,
- );
- }
- }, [
- settings.followDeviceOrientation,
- settings.defaultVideoOrientation,
- segments,
- ]);
-
- useEffect(() => {
- if (Platform.isTV) {
- return;
- }
-
- const subscription = AppState.addEventListener("change", (nextAppState) => {
- if (
- appState.current.match(/inactive|background/) &&
- nextAppState === "active"
- ) {
- BackGroundDownloader.checkForExistingDownloads().catch(
- (error: unknown) => {
- writeErrorLog("Failed to resume background downloads", error);
- },
- );
- }
- });
-
- BackGroundDownloader.checkForExistingDownloads().catch((error: unknown) => {
- writeErrorLog("Failed to resume background downloads", error);
- });
- return () => {
- subscription.remove();
- };
- }, []);
+ }, [user]);
return (
@@ -420,52 +333,55 @@ function Layout() {
-
-
-
-
- null,
+
+
+
+
+
+ null,
+ }}
+ />
+ null,
+ }}
+ />
+
+
+
+
- null,
- }}
- />
-
-
-
-
-
-
+
+
+
+
diff --git a/app/login.tsx b/app/login.tsx
index d64516b6..d941cece 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -4,17 +4,16 @@ import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { t } from "i18next";
import { useAtomValue } from "jotai";
-import type React from "react";
import { useCallback, useEffect, useState } from "react";
import {
Alert,
Keyboard,
KeyboardAvoidingView,
Platform,
- SafeAreaView,
TouchableOpacity,
View,
} from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
import { z } from "zod";
import { Button } from "@/components/Button";
import { Input } from "@/components/common/Input";
@@ -63,12 +62,13 @@ const Login: React.FC = () => {
address: _apiUrl,
});
+ // Wait for server setup and state updates to complete
setTimeout(() => {
if (_username && _password) {
setCredentials({ username: _username, password: _password });
login(_username, _password);
}
- }, 300);
+ }, 0);
}
})();
}, [_apiUrl, _username, _password]);
@@ -82,10 +82,10 @@ const Login: React.FC = () => {
onPress={() => {
removeServer();
}}
- className='flex flex-row items-center'
+ className='flex flex-row items-center pr-2 pl-1'
>
-
+
{t("login.change_server")}
@@ -371,10 +371,11 @@ const Login: React.FC = () => {
// Mobile layout
{api?.basePath ? (
-
+
@@ -443,7 +444,7 @@ const Login: React.FC = () => {
) : (
-
+
(key: string): T | undefined {
+(storage as any).get = function (key: string): T | undefined {
try {
const serializedItem = this.getString(key);
if (!serializedItem) return undefined;
@@ -20,10 +20,10 @@ MMKV.prototype.get = function (key: string): T | undefined {
}
};
-MMKV.prototype.setAny = function (key: string, value: any | undefined): void {
+(storage as any).setAny = function (key: string, value: any | undefined): void {
try {
if (value === undefined) {
- this.delete(key);
+ this.remove(key);
} else {
this.set(key, JSON.stringify(value));
}
diff --git a/babel.config.js b/babel.config.js
index 41dc7e41..012913ae 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -2,6 +2,6 @@ module.exports = (api) => {
api.cache(true);
return {
presets: ["babel-preset-expo"],
- plugins: ["nativewind/babel", "react-native-reanimated/plugin"],
+ plugins: ["nativewind/babel", "react-native-worklets/plugin"],
};
};
diff --git a/bun.lock b/bun.lock
index 42385190..76012d57 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,102 +4,109 @@
"": {
"name": "streamyfin",
"dependencies": {
- "@bottom-tabs/react-navigation": "^0.12.0",
- "@expo/metro-runtime": "~5.0.5",
+ "@bottom-tabs/react-navigation": "^1.0.2",
+ "@expo/metro-runtime": "~6.1.1",
"@expo/react-native-action-sheet": "^4.1.1",
- "@expo/vector-icons": "^14.1.0",
+ "@expo/ui": "^0.2.0-beta.4",
+ "@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "^5.1.0",
- "@jellyfin/sdk": "^0.11.0",
- "@kesha-antonov/react-native-background-downloader": "^3.2.6",
+ "@jellyfin/sdk": "^0.13.0",
"@react-native-community/netinfo": "^11.4.1",
- "@react-native-menu/menu": "1.2.3",
"@react-navigation/material-top-tabs": "^7.2.14",
"@react-navigation/native": "^7.0.14",
- "@shopify/flash-list": "^1.8.3",
+ "@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
- "expo": "^53.0.23",
- "expo-application": "~6.1.4",
- "expo-asset": "~11.1.7",
- "expo-background-task": "~0.2.8",
- "expo-blur": "~14.1.4",
- "expo-brightness": "~13.1.4",
- "expo-build-properties": "~0.14.6",
- "expo-constants": "~17.1.5",
- "expo-device": "~7.1.4",
- "expo-font": "~13.3.1",
- "expo-haptics": "~14.1.4",
- "expo-image": "~2.4.0",
- "expo-linear-gradient": "~14.1.4",
- "expo-linking": "~7.1.4",
- "expo-localization": "~16.1.5",
- "expo-notifications": "~0.31.2",
- "expo-router": "~5.1.7",
- "expo-screen-orientation": "~8.1.6",
- "expo-sensors": "~14.1.4",
- "expo-sharing": "~13.1.5",
- "expo-splash-screen": "~0.30.8",
- "expo-status-bar": "~2.2.3",
- "expo-system-ui": "~5.0.11",
- "expo-task-manager": "~13.1.6",
- "expo-web-browser": "~14.2.0",
+ "expo": "^54.0.23",
+ "expo-application": "~7.0.5",
+ "expo-asset": "~12.0.6",
+ "expo-background-task": "~1.0.5",
+ "expo-blur": "~15.0.5",
+ "expo-brightness": "~14.0.5",
+ "expo-build-properties": "~1.0.6",
+ "expo-constants": "~18.0.10",
+ "expo-dev-client": "~6.0.17",
+ "expo-device": "~8.0.5",
+ "expo-font": "~14.0.9",
+ "expo-haptics": "~15.0.5",
+ "expo-image": "~3.0.10",
+ "expo-linear-gradient": "~15.0.5",
+ "expo-linking": "~8.0.6",
+ "expo-localization": "~17.0.5",
+ "expo-notifications": "~0.32.7",
+ "expo-router": "~6.0.14",
+ "expo-screen-orientation": "~9.0.5",
+ "expo-sensors": "~15.0.5",
+ "expo-sharing": "~14.0.5",
+ "expo-splash-screen": "~31.0.7",
+ "expo-status-bar": "~3.0.6",
+ "expo-system-ui": "~6.0.8",
+ "expo-task-manager": "~14.0.8",
+ "expo-web-browser": "~15.0.9",
"i18next": "^25.0.0",
"jotai": "^2.12.5",
"lodash": "^4.17.21",
"nativewind": "^2.0.11",
- "react": "19.0.0",
- "react-dom": "19.0.0",
+ "patch-package": "^8.0.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
"react-i18next": "^15.4.0",
- "react-native": "npm:react-native-tvos@0.79.5-0",
+ "react-native": "npm:react-native-tvos@0.81.5-1",
"react-native-awesome-slider": "^2.9.0",
- "react-native-bottom-tabs": "^0.12.0",
+ "react-native-bottom-tabs": "^1.0.2",
"react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2",
"react-native-country-flag": "^2.0.2",
"react-native-device-info": "^14.0.4",
- "react-native-gesture-handler": "~2.24.0",
- "react-native-google-cast": "^4.9.0",
+ "react-native-edge-to-edge": "^1.7.0",
+ "react-native-gesture-handler": "~2.28.0",
+ "react-native-google-cast": "^4.9.1",
"react-native-image-colors": "^2.4.0",
- "react-native-ios-context-menu": "^3.1.0",
- "react-native-ios-utilities": "5.1.8",
- "react-native-mmkv": "2.12.2",
+ "react-native-ios-context-menu": "^3.2.1",
+ "react-native-ios-utilities": "5.2.0",
+ "react-native-mmkv": "4.0.0",
+ "react-native-nitro-modules": "^0.31.5",
"react-native-pager-view": "^6.9.1",
- "react-native-reanimated": "~3.19.1",
+ "react-native-reanimated": "~4.1.1",
"react-native-reanimated-carousel": "4.0.2",
- "react-native-safe-area-context": "5.4.0",
- "react-native-screens": "~4.11.1",
- "react-native-svg": "15.11.2",
+ "react-native-safe-area-context": "~5.6.0",
+ "react-native-screens": "~4.18.0",
+ "react-native-svg": "15.12.1",
"react-native-udp": "^4.1.7",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
- "react-native-video": "6.14.1",
+ "react-native-video": "6.16.1",
"react-native-volume-manager": "^2.0.8",
- "react-native-web": "^0.20.0",
+ "react-native-web": "^0.21.0",
+ "react-native-worklets": "0.5.1",
"sonner-native": "^0.21.0",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
- "zeego": "^3.0.6",
"zod": "^4.1.3",
},
"devDependencies": {
- "@babel/core": "7.28.5",
- "@biomejs/biome": "2.2.7",
- "@react-native-community/cli": "20.0.2",
- "@react-native-tvos/config-tv": "0.1.4",
- "@types/jest": "30.0.0",
- "@types/lodash": "4.17.20",
- "@types/react": "~19.0.10",
- "@types/react-test-renderer": "19.1.0",
- "cross-env": "10.1.0",
- "expo-dev-client": "5.2.4",
- "expo-doctor": "1.17.11",
- "husky": "9.1.7",
- "lint-staged": "16.2.6",
+ "@babel/core": "^7.20.0",
+ "@biomejs/biome": "^2.2.4",
+ "@react-native-community/cli": "^20.0.0",
+ "@react-native-tvos/config-tv": "^0.1.1",
+ "@types/jest": "^29.5.12",
+ "@types/lodash": "^4.17.15",
+ "@types/react": "~19.1.10",
+ "@types/react-test-renderer": "^19.0.0",
+ "cross-env": "^10.0.0",
+ "expo-doctor": "^1.17.0",
+ "husky": "^9.1.7",
+ "lint-staged": "^16.1.5",
+ "postinstall-postinstall": "^2.1.0",
"react-test-renderer": "19.1.1",
- "typescript": "5.8.3",
+ "typescript": "~5.9.2",
},
},
},
+ "overrides": {
+ "expo-constants": "~18.0.10",
+ "expo-task-manager": "~14.0.8",
+ },
"packages": {
"@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="],
@@ -211,6 +218,8 @@
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="],
+ "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="],
+
"@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="],
"@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="],
@@ -291,25 +300,25 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
- "@biomejs/biome": ["@biomejs/biome@2.2.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.7", "@biomejs/cli-darwin-x64": "2.2.7", "@biomejs/cli-linux-arm64": "2.2.7", "@biomejs/cli-linux-arm64-musl": "2.2.7", "@biomejs/cli-linux-x64": "2.2.7", "@biomejs/cli-linux-x64-musl": "2.2.7", "@biomejs/cli-win32-arm64": "2.2.7", "@biomejs/cli-win32-x64": "2.2.7" }, "bin": { "biome": "bin/biome" } }, "sha512-1a8j0UP1vXVUf3UzMZEJ/zS2VgAG6wU6Cuh/I764sUGI+MCnJs/9WaojHYBDCxCMLTgU60/WqnYof85emXmSBA=="],
+ "@biomejs/biome": ["@biomejs/biome@2.3.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.4", "@biomejs/cli-darwin-x64": "2.3.4", "@biomejs/cli-linux-arm64": "2.3.4", "@biomejs/cli-linux-arm64-musl": "2.3.4", "@biomejs/cli-linux-x64": "2.3.4", "@biomejs/cli-linux-x64-musl": "2.3.4", "@biomejs/cli-win32-arm64": "2.3.4", "@biomejs/cli-win32-x64": "2.3.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w=="],
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xBUUsebnO2/Qj1v7eZmKUy2ZcFkZ4/jLUkxN02Qup1RPoRaiW9AKXHrqS3L7iX6PzofHY2xuZ+Pb9kAcpoe0qA=="],
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w40GvlNzLaqmuWYiDU6Ys9FNhJiclngKqcGld3iJIiy2bpJ0Q+8n3haiaC81uTPY/NA0d8Q/I3Z9+ajc14102Q=="],
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-vsY4NhmxqgfLJufr9XUnC+yGUPJiXAc1mz6FcjaAmuIuLwfghN4uQO7hnW2AneGyoi2mNe9Jbvf6Qtq4AjzrFg=="],
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-3s7TLVtjJ7ni1xADXsS7x7GMUrLBZXg8SemXc3T0XLslzvqKj/dq1xGeBQ+pOWQzng9MaozfacIHdK2UlJ3jGA=="],
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-nUdco104rjV9dULi1VssQ5R/kX2jE/Z2sDjyqS+siV9sTQda0DwmEUixFNRCWvZJRRiZUWhgiDFJ4n7RowO8Mg=="],
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA=="],
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-FrTwvKO/7t5HbVTvhlMOTOVQLAcR7r4O4iFQhEpZXUtBfosHqrX/JJlX7daPawoe14MDcCu9CDg0zLVpTuDvuQ=="],
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-IruVGQRwMURivWazchiq7gKAqZSFs5so6gi0hJyxk7x6HR+iwZbO2IxNOqyLURBvL06qkIHs7Wffl6Bw30vCbQ=="],
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-tPTcGAIEOOZrj2tQ7fdraWlaxNKApBw6l4In8wQQV1IyxnAexqi0hykHzKEX8hKKctf5gxGBfNCzyIvqpj4CFQ=="],
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q=="],
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-MnsysF5s/iLC5wnYvuMseOy+m8Pd4bWG1uwlVyy2AUbfjAVUgtbYbboc5wMXljFrDY7e6rLjLTR4S2xqDpGlQg=="],
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA=="],
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-h5D1jhwA2b7cFXerYiJfXHSzzAMFFoEDL5Mc2BgiaEw0iaSgSso/3Nc6FbOR55aTQISql+IpB4PS7JoV26Gdbw=="],
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw=="],
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.7", "", { "os": "win32", "cpu": "x64" }, "sha512-URqAJi0kONyBKG4V9NVafHLDtm6IHmF4qPYi/b6x7MD6jxpWeJiTCO6R5+xDlWckX2T/OGv6Yq3nkz6s0M8Ykw=="],
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-FGCijXecmC4IedQ0esdYNlMpx0Jxgf4zceCaMu6fkjWyjgn50ZQtMiqZZQ0Q/77yqPxvtkgZAvt5uGw0gAAjig=="],
- "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.12.2", "", { "dependencies": { "color": "^5.0.0" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-vQ/7pNcWk2TgveVCBfdQnbLC6zaRbIL2EM4Vsifk/NCCZ49oT1G2K7iXAsCPby9/ofd6ndGOMmsVLFVq5M7fjw=="],
+ "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@1.0.2", "", { "dependencies": { "color": "^5.0.0" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-OrCw8s2NzFxO1TO5W2vyr7HNvh1Yjy00f72D/0BIPtImc0aj5CRrT9nFRE7YP0FWZb0AY5+0QU9jaoph1rBlSg=="],
"@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="],
@@ -317,37 +326,43 @@
"@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
- "@expo/cli": ["@expo/cli@0.24.22", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~11.0.13", "@expo/config-plugins": "~10.1.2", "@expo/devcert": "^1.1.2", "@expo/env": "~1.0.7", "@expo/image-utils": "^0.7.6", "@expo/json-file": "^9.1.5", "@expo/metro-config": "~0.20.17", "@expo/osascript": "^2.2.5", "@expo/package-manager": "^1.8.6", "@expo/plist": "^0.3.5", "@expo/prebuild-config": "^9.0.12", "@expo/schema-utils": "^0.1.0", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.79.6", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^10.4.2", "lan-network": "^0.1.6", "minimatch": "^9.0.0", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^7.4.3", "terminal-link": "^2.1.1", "undici": "^6.18.2", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "bin": { "expo-internal": "build/bin/cli" } }, "sha512-cEg6/F8ZWjoVkEwm0rXKReWbsCUROFbLFBYht+d5RzHnDwJoTX4QWJKx4m+TGNDPamRUIGw36U4z41Fvev0XmA=="],
+ "@expo/cli": ["@expo/cli@54.0.16", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~12.0.10", "@expo/config-plugins": "~54.0.2", "@expo/devcert": "^1.1.2", "@expo/env": "~2.0.7", "@expo/image-utils": "^0.8.7", "@expo/json-file": "^10.0.7", "@expo/mcp-tunnel": "~0.1.0", "@expo/metro": "~54.1.0", "@expo/metro-config": "~54.0.9", "@expo/osascript": "^2.3.7", "@expo/package-manager": "^1.9.8", "@expo/plist": "^0.4.7", "@expo/prebuild-config": "^54.0.6", "@expo/schema-utils": "^0.1.7", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.81.5", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "expo-server": "^1.0.4", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^10.4.2", "lan-network": "^0.1.6", "minimatch": "^9.0.0", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^7.4.3", "terminal-link": "^2.1.1", "undici": "^6.18.2", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-hY/OdRaJMs5WsVPuVSZ+RLH3VObJmL/pv5CGCHEZHN2PxZjSZSdctyKV8UcFBXTF0yIKNAJ9XLs1dlNYXHh4Cw=="],
"@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="],
- "@expo/config": ["@expo/config@11.0.13", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~10.1.2", "@expo/config-types": "^53.0.5", "@expo/json-file": "^9.1.5", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-TnGb4u/zUZetpav9sx/3fWK71oCPaOjZHoVED9NaEncktAd0Eonhq5NUghiJmkUGt3gGSjRAEBXiBbbY9/B1LA=="],
+ "@expo/config": ["@expo/config@12.0.10", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.2", "@expo/config-types": "^54.0.8", "@expo/json-file": "^10.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-lJMof5Nqakq1DxGYlghYB/ogSBjmv4Fxn1ovyDmcjlRsQdFCXgu06gEUogkhPtc9wBt9WlTTfqENln5HHyLW6w=="],
- "@expo/config-plugins": ["@expo/config-plugins@10.1.2", "", { "dependencies": { "@expo/config-types": "^53.0.5", "@expo/json-file": "~9.1.5", "@expo/plist": "^0.3.5", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-IMYCxBOcnuFStuK0Ay+FzEIBKrwW8OVUMc65+v0+i7YFIIe8aL342l7T4F8lR4oCfhXn7d6M5QPgXvjtc/gAcw=="],
+ "@expo/config-plugins": ["@expo/config-plugins@54.0.2", "", { "dependencies": { "@expo/config-types": "^54.0.8", "@expo/json-file": "~10.0.7", "@expo/plist": "^0.4.7", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^10.4.2", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-jD4qxFcURQUVsUFGMcbo63a/AnviK8WUGard+yrdQE3ZrB/aurn68SlApjirQQLEizhjI5Ar2ufqflOBlNpyPg=="],
- "@expo/config-types": ["@expo/config-types@53.0.5", "", {}, "sha512-kqZ0w44E+HEGBjy+Lpyn0BVL5UANg/tmNixxaRMLS6nf37YsDrLk2VMAmeKMMk5CKG0NmOdVv3ngeUjRQMsy9g=="],
+ "@expo/config-types": ["@expo/config-types@54.0.8", "", {}, "sha512-lyIn/x/Yz0SgHL7IGWtgTLg6TJWC9vL7489++0hzCHZ4iGjVcfZmPTUfiragZ3HycFFj899qN0jlhl49IHa94A=="],
"@expo/devcert": ["@expo/devcert@1.2.0", "", { "dependencies": { "@expo/sudo-prompt": "^9.3.1", "debug": "^3.1.0", "glob": "^10.4.2" } }, "sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA=="],
- "@expo/env": ["@expo/env@1.0.7", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-qSTEnwvuYJ3umapO9XJtrb1fAqiPlmUUg78N0IZXXGwQRt+bkp0OBls+Y5Mxw/Owj8waAM0Z3huKKskRADR5ow=="],
+ "@expo/devtools": ["@expo/devtools@0.1.7", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" }, "optionalPeers": ["react", "react-native"] }, "sha512-dfIa9qMyXN+0RfU6SN4rKeXZyzKWsnz6xBSDccjL4IRiE+fQ0t84zg0yxgN4t/WK2JU5v6v4fby7W7Crv9gJvA=="],
- "@expo/fingerprint": ["@expo/fingerprint@0.13.4", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "find-up": "^5.0.0", "getenv": "^2.0.0", "glob": "^10.4.2", "ignore": "^5.3.1", "minimatch": "^9.0.0", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-MYfPYBTMfrrNr07DALuLhG6EaLVNVrY/PXjEzsjWdWE4ZFn0yqI0IdHNkJG7t1gePT8iztHc7qnsx+oo/rDo6w=="],
+ "@expo/env": ["@expo/env@2.0.7", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-BNETbLEohk3HQ2LxwwezpG8pq+h7Fs7/vAMP3eAtFT1BCpprLYoBBFZH7gW4aqGfqOcVP4Lc91j014verrYNGg=="],
- "@expo/image-utils": ["@expo/image-utils@0.7.6", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-GKnMqC79+mo/1AFrmAcUcGfbsXXTRqOMNS1umebuevl3aaw+ztsYEFEiuNhHZW7PQ3Xs3URNT513ZxKhznDscw=="],
+ "@expo/fingerprint": ["@expo/fingerprint@0.15.3", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^10.4.2", "ignore": "^5.3.1", "minimatch": "^9.0.0", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-8YPJpEYlmV171fi+t+cSLMX1nC5ngY9j2FiN70dHldLpd6Ct6ouGhk96svJ4BQZwsqwII2pokwzrDAwqo4Z0FQ=="],
- "@expo/json-file": ["@expo/json-file@9.1.5", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA=="],
+ "@expo/image-utils": ["@expo/image-utils@0.8.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "resolve-global": "^1.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-SXOww4Wq3RVXLyOaXiCCuQFguCDh8mmaHBv54h/R29wGl4jRY8GEyQEx8SypV/iHt1FbzsU/X3Qbcd9afm2W2w=="],
- "@expo/metro-config": ["@expo/metro-config@0.20.17", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~11.0.12", "@expo/env": "~1.0.7", "@expo/json-file": "~9.1.5", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "debug": "^4.3.2", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0", "glob": "^10.4.2", "jsc-safe-url": "^0.2.4", "lightningcss": "~1.27.0", "minimatch": "^9.0.0", "postcss": "~8.4.32", "resolve-from": "^5.0.0" } }, "sha512-lpntF2UZn5bTwrPK6guUv00Xv3X9mkN3YYla+IhEHiYXWyG7WKOtDU0U4KR8h3ubkZ6SPH3snDyRyAzMsWtZFA=="],
+ "@expo/json-file": ["@expo/json-file@10.0.7", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw=="],
- "@expo/metro-runtime": ["@expo/metro-runtime@5.0.5", "", { "peerDependencies": { "react-native": "*" } }, "sha512-P8UFTi+YsmiD1BmdTdiIQITzDMcZgronsA3RTQ4QKJjHM3bas11oGzLQOnFaIZnlEV8Rrr3m1m+RHxvnpL+t/A=="],
+ "@expo/mcp-tunnel": ["@expo/mcp-tunnel@0.1.0", "", { "dependencies": { "ws": "^8.18.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.13.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-rJ6hl0GnIZj9+ssaJvFsC7fwyrmndcGz+RGFzu+0gnlm78X01957yjtHgjcmnQAgL5hWEOR6pkT0ijY5nU5AWw=="],
+
+ "@expo/metro": ["@expo/metro@54.1.0", "", { "dependencies": { "metro": "0.83.2", "metro-babel-transformer": "0.83.2", "metro-cache": "0.83.2", "metro-cache-key": "0.83.2", "metro-config": "0.83.2", "metro-core": "0.83.2", "metro-file-map": "0.83.2", "metro-resolver": "0.83.2", "metro-runtime": "0.83.2", "metro-source-map": "0.83.2", "metro-transform-plugins": "0.83.2", "metro-transform-worker": "0.83.2" } }, "sha512-MgdeRNT/LH0v1wcO0TZp9Qn8zEF0X2ACI0wliPtv5kXVbXWI+yK9GyrstwLAiTXlULKVIg3HVSCCvmLu0M3tnw=="],
+
+ "@expo/metro-config": ["@expo/metro-config@54.0.9", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~12.0.10", "@expo/env": "~2.0.7", "@expo/json-file": "~10.0.7", "@expo/metro": "~54.1.0", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0", "glob": "^10.4.2", "hermes-parser": "^0.29.1", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "minimatch": "^9.0.0", "postcss": "~8.4.32", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-CRI4WgFXrQ2Owyr8q0liEBJveUIF9DcYAKadMRsJV7NxGNBdrIIKzKvqreDfsGiRqivbLsw6UoNb3UE7/SvPfg=="],
+
+ "@expo/metro-runtime": ["@expo/metro-runtime@6.1.2", "", { "dependencies": { "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g=="],
"@expo/osascript": ["@expo/osascript@2.3.7", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "exec-async": "^2.2.0" } }, "sha512-IClSOXxR0YUFxIriUJVqyYki7lLMIHrrzOaP01yxAL1G8pj2DWV5eW1y5jSzIcIfSCNhtGsshGd1tU/AYup5iQ=="],
"@expo/package-manager": ["@expo/package-manager@1.9.8", "", { "dependencies": { "@expo/json-file": "^10.0.7", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-4/I6OWquKXYnzo38pkISHCOCOXxfeEmu4uDoERq1Ei/9Ur/s9y3kLbAamEkitUkDC7gHk1INxRWEfFNzGbmOrA=="],
- "@expo/plist": ["@expo/plist@0.3.5", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.2.3", "xmlbuilder": "^15.1.1" } }, "sha512-9RYVU1iGyCJ7vWfg3e7c/NVyMFs8wbl+dMWZphtFtsqyN9zppGREU3ctlD3i8KUE0sCUTVnLjCWr+VeUIDep2g=="],
+ "@expo/plist": ["@expo/plist@0.4.7", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.2.3", "xmlbuilder": "^15.1.1" } }, "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA=="],
- "@expo/prebuild-config": ["@expo/prebuild-config@9.0.12", "", { "dependencies": { "@expo/config": "~11.0.13", "@expo/config-plugins": "~10.1.2", "@expo/config-types": "^53.0.5", "@expo/image-utils": "^0.7.6", "@expo/json-file": "^9.1.5", "@react-native/normalize-colors": "0.79.6", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" } }, "sha512-AKH5Scf+gEMgGxZZaimrJI2wlUJlRoqzDNn7/rkhZa5gUTnO4l6slKak2YdaH+nXlOWCNfAQWa76NnpQIfmv6Q=="],
+ "@expo/prebuild-config": ["@expo/prebuild-config@54.0.6", "", { "dependencies": { "@expo/config": "~12.0.10", "@expo/config-plugins": "~54.0.2", "@expo/config-types": "^54.0.8", "@expo/image-utils": "^0.8.7", "@expo/json-file": "^10.0.7", "@react-native/normalize-colors": "0.81.5", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-xowuMmyPNy+WTNq+YX0m0EFO/Knc68swjThk4dKivgZa8zI1UjvFXOBIOp8RX4ljCXLzwxQJM5oBBTvyn+59ZA=="],
"@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.1", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-4KRaba2vhqDRR7ObBj6nrD5uJw8ePoNHdIOMETTpgGTX7StUbrF4j/sfrP1YUyaPEa1P8FXdwG6pB+2WtrJd1A=="],
@@ -355,26 +370,18 @@
"@expo/sdk-runtime-versions": ["@expo/sdk-runtime-versions@1.0.0", "", {}, "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="],
- "@expo/server": ["@expo/server@0.6.3", "", { "dependencies": { "abort-controller": "^3.0.0", "debug": "^4.3.4", "source-map-support": "~0.5.21", "undici": "^6.18.2 || ^7.0.0" } }, "sha512-Ea7NJn9Xk1fe4YeJ86rObHSv/bm3u/6WiQPXEqXJ2GrfYpVab2Swoh9/PnSM3KjR64JAgKjArDn1HiPjITCfHA=="],
-
"@expo/spawn-async": ["@expo/spawn-async@1.7.2", "", { "dependencies": { "cross-spawn": "^7.0.3" } }, "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew=="],
"@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="],
- "@expo/vector-icons": ["@expo/vector-icons@14.1.0", "", { "peerDependencies": { "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ=="],
+ "@expo/ui": ["@expo/ui@0.2.0-canary-20251031-b135dff", "", { "dependencies": { "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-L/TEKnv/hpQ/Q1sO8lJw0wxdcv88UoA1JShwRSYHLN88UstjxvBNvMqlKGk7SNkTUJtlrttWAundJA4jM2mDPw=="],
+
+ "@expo/vector-icons": ["@expo/vector-icons@15.0.3", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA=="],
"@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="],
"@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="],
- "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
-
- "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
-
- "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
-
- "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
-
"@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.6", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-native": "*", "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.16.0 || >=4.0.0-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-vmruJxdiUGDg+ZYcDmS30XDhq/h/+QkINOI5LY/uGjx8cPGwgJW0H6AB902gNTKtccbiKe/rr94EwdmIEz+LAQ=="],
"@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="],
@@ -395,23 +402,17 @@
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="],
- "@jellyfin/sdk": ["@jellyfin/sdk@0.11.0", "", { "peerDependencies": { "axios": "^1.3.4" } }, "sha512-WmM4as9ptqH+CvC2YsUefNWQDmu2aWIamwAoj7h2BFR6l019pcRFG5FT22egwbdizR6DfdpmsoAWB4x9QCzcEQ=="],
+ "@jellyfin/sdk": ["@jellyfin/sdk@0.13.0", "", { "peerDependencies": { "axios": "^1.12.0" } }, "sha512-oiBAOXH6s+dKdReSsYgNktBDzbxtg4JVWhEzIxZSxKcWMdSKmBtK41MhXRO7IWAC40DguKUm3nU/Z493qPAlWA=="],
"@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="],
- "@jest/diff-sequences": ["@jest/diff-sequences@30.0.1", "", {}, "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw=="],
-
"@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="],
- "@jest/expect-utils": ["@jest/expect-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0" } }, "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA=="],
+ "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="],
"@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="],
- "@jest/get-type": ["@jest/get-type@30.1.0", "", {}, "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA=="],
-
- "@jest/pattern": ["@jest/pattern@30.0.1", "", { "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" } }, "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA=="],
-
- "@jest/schemas": ["@jest/schemas@30.0.5", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA=="],
+ "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
"@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="],
@@ -449,8 +450,6 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
- "@kesha-antonov/react-native-background-downloader": ["@kesha-antonov/react-native-background-downloader@3.2.6", "", { "peerDependencies": { "react-native": ">=0.57.0" } }, "sha512-J87PHzBh4knWTQNkCNM4LTMZ85RpMW/QSV+0LGdTxz4JmfLXoeg8R6ratbFU0DP/l8K1eL7r4S1Rc8bmqNJ3Ug=="],
-
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -461,32 +460,24 @@
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
- "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
-
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
- "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
+ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
- "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
-
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
- "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
-
- "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
-
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
@@ -497,6 +488,8 @@
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
+
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
@@ -507,12 +500,6 @@
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
- "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
-
- "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
-
- "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
-
"@react-native-community/cli": ["@react-native-community/cli@20.0.2", "", { "dependencies": { "@react-native-community/cli-clean": "20.0.2", "@react-native-community/cli-config": "20.0.2", "@react-native-community/cli-doctor": "20.0.2", "@react-native-community/cli-server-api": "20.0.2", "@react-native-community/cli-tools": "20.0.2", "@react-native-community/cli-types": "20.0.2", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-ocgRFKRLX8b5rEK38SJfpr0AMl6SqseWljk6c5LxCG/zpCfPPNQdXq1OsDvmEwsqO4OEQ6tmOaSm9OgTm6FhbQ=="],
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.0.2", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-hfbC69fTD0fqZCCep8aqnVztBXUhAckNhi76lEV7USENtgBRwNq2s1wATgKAzOhxKuAL9TEkf5TZ/Dhp/YLhCQ=="],
@@ -539,47 +526,45 @@
"@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="],
- "@react-native-menu/menu": ["@react-native-menu/menu@1.2.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-sEfiVIivsa0lSelFm9Wbm/RAi+XoEHc75GGhjwvSrj9KSCVvNNXwr9F8l42e1t/lzYvVYzmkYxLG6VKxrDYJiw=="],
-
"@react-native-tvos/config-tv": ["@react-native-tvos/config-tv@0.1.4", "", { "dependencies": { "getenv": "^1.0.0" }, "peerDependencies": { "expo": ">=52.0.0" } }, "sha512-xfVDqSFjEUsb+xcMk0hE2Z/M6QZH0QzAJOSQZwo7W/ZRaLrd+xFQnx0LaXqt3kxlR3P7wskKHByDP/FSoUZnbA=="],
- "@react-native-tvos/virtualized-lists": ["@react-native-tvos/virtualized-lists@0.79.5-0", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.0.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-VvZyBraMdMctuT9sjv1XOirh38PaPQwXjdagfiSnn2Vx5P2X/EHqiqx+UkKZK1AcxUK2dQCI+roXZboRXIB+Bw=="],
+ "@react-native-tvos/virtualized-lists": ["@react-native-tvos/virtualized-lists@0.81.5-1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-v77jJvzH2jzMj3G8pthdaRjiUhmdQ3S/OGiTX45Tn1J+whLaPOEkVRCel9xPHhrTPIEwrOOwGNiAFN/s1hzWZA=="],
- "@react-native/assets-registry": ["@react-native/assets-registry@0.79.5", "", {}, "sha512-N4Kt1cKxO5zgM/BLiyzuuDNquZPiIgfktEQ6TqJ/4nKA8zr4e8KJgU6Tb2eleihDO4E24HmkvGc73naybKRz/w=="],
+ "@react-native/assets-registry": ["@react-native/assets-registry@0.81.5", "", {}, "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w=="],
- "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.79.6", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.79.6" } }, "sha512-CS5OrgcMPixOyUJ/Sk/HSsKsKgyKT5P7y3CojimOQzWqRZBmoQfxdST4ugj7n1H+ebM2IKqbgovApFbqXsoX0g=="],
+ "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.5" } }, "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ=="],
- "@react-native/babel-preset": ["@react-native/babel-preset@0.79.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.79.6", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-H+FRO+r2Ql6b5IwfE0E7D52JhkxjeGSBSUpCXAI5zQ60zSBJ54Hwh2bBJOohXWl4J+C7gKYSAd2JHMUETu+c/A=="],
+ "@react-native/babel-preset": ["@react-native/babel-preset@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.81.5", "babel-plugin-syntax-hermes-parser": "0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA=="],
- "@react-native/codegen": ["@react-native/codegen@0.79.5", "", { "dependencies": { "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ=="],
+ "@react-native/codegen": ["@react-native/codegen@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g=="],
- "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.79.5", "", { "dependencies": { "@react-native/dev-middleware": "0.79.5", "chalk": "^4.0.0", "debug": "^2.2.0", "invariant": "^2.2.4", "metro": "^0.82.0", "metro-config": "^0.82.0", "metro-core": "^0.82.0", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*" }, "optionalPeers": ["@react-native-community/cli"] }, "sha512-ApLO1ARS8JnQglqS3JAHk0jrvB+zNW3dvNJyXPZPoygBpZVbf8sjvqeBiaEYpn8ETbFWddebC4HoQelDndnrrA=="],
+ "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.81.5", "", { "dependencies": { "@react-native/dev-middleware": "0.81.5", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.1", "metro-config": "^0.83.1", "metro-core": "^0.83.1", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw=="],
- "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.79.6", "", {}, "sha512-lIK/KkaH7ueM22bLO0YNaQwZbT/oeqhaghOvmZacaNVbJR1Cdh/XAqjT8FgCS+7PUnbxA8B55NYNKGZG3O2pYw=="],
+ "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.81.5", "", {}, "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w=="],
- "@react-native/dev-middleware": ["@react-native/dev-middleware@0.79.6", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.79.6", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-BK3GZBa9c7XSNR27EDRtxrgyyA3/mf1j3/y+mPk7Ac0Myu85YNrXnC9g3mL5Ytwo0g58TKrAIgs1fF2Q5Mn6mQ=="],
+ "@react-native/dev-middleware": ["@react-native/dev-middleware@0.81.5", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.81.5", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA=="],
- "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.79.5", "", {}, "sha512-K3QhfFNKiWKF3HsCZCEoWwJPSMcPJQaeqOmzFP4RL8L3nkpgUwn74PfSCcKHxooVpS6bMvJFQOz7ggUZtNVT+A=="],
+ "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.81.5", "", {}, "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg=="],
- "@react-native/js-polyfills": ["@react-native/js-polyfills@0.79.5", "", {}, "sha512-a2wsFlIhvd9ZqCD5KPRsbCQmbZi6KxhRN++jrqG0FUTEV5vY7MvjjUqDILwJd2ZBZsf7uiDuClCcKqA+EEdbvw=="],
+ "@react-native/js-polyfills": ["@react-native/js-polyfills@0.81.5", "", {}, "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w=="],
- "@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.6", "", {}, "sha512-0v2/ruY7eeKun4BeKu+GcfO+SHBdl0LJn4ZFzTzjHdWES0Cn+ONqKljYaIv8p9MV2Hx/kcdEvbY4lWI34jC/mQ=="],
+ "@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.5", "", {}, "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g=="],
- "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.7.2", "", { "dependencies": { "@react-navigation/elements": "^2.8.0", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-IA1+N9VrZPQiUmNI/T4co/zXyxSZ1m07pbRUBtnZAwqrybNLZh3qsfPIvy+jL89owyGgZEw1UUscHT0EyEhkbQ=="],
+ "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.8.4", "", { "dependencies": { "@react-navigation/elements": "^2.8.1", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-Ie+7EgUxfZmVXm4RCiJ96oaiwJVFgVE8NJoeUKLLcYEB/99wKbhuKPJNtbkpR99PHfhq64SE7476BpcP4xOFhw=="],
"@react-navigation/core": ["@react-navigation/core@7.13.0", "", { "dependencies": { "@react-navigation/routers": "^7.5.1", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-Fc/SO23HnlGnkou/z8JQUzwEMvhxuUhr4rdPTIZp/c8q1atq3k632Nfh8fEiGtk+MP1wtIvXdN2a5hBIWpLq3g=="],
- "@react-navigation/elements": ["@react-navigation/elements@2.8.0", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-uvSOkYOF7wfgkt57cl+6fZ2vQgTiYYyJleZzuWthPKHK4nDq2M4sc9SSzgK9GS9UCJFRBErNtl3S+/ErtrwREw=="],
+ "@react-navigation/elements": ["@react-navigation/elements@2.8.1", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ=="],
- "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.4.1", "", { "dependencies": { "@react-navigation/elements": "^2.8.0", "color": "^4.2.3", "react-native-tab-view": "^4.2.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-dox0p78P+dScyRBsCUrSITjG/iXeT/QAj+AS2viBfE2Odr+CmnYbNYawMyAQO/GzYB5ImqP4lgNvP+qBrCypMA=="],
+ "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.4.2", "", { "dependencies": { "@react-navigation/elements": "^2.8.1", "color": "^4.2.3", "react-native-tab-view": "^4.2.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-LB/bCDhdaKsexA5w0otgZEDBysGbiCr2l0hW6z41rJQ0JqAOVybH0cBuFr3Awasv0mQh9iTJNha4VsuUb7Q0Xw=="],
"@react-navigation/native": ["@react-navigation/native@7.1.19", "", { "dependencies": { "@react-navigation/core": "^7.13.0", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-fM7q8di4Q8sp2WUhiUWOe7bEDRyRhbzsKQOd5N2k+lHeCx3UncsRYuw4Q/KN0EovM3wWKqMMmhy/YWuEO04kgw=="],
- "@react-navigation/native-stack": ["@react-navigation/native-stack@7.6.1", "", { "dependencies": { "@react-navigation/elements": "^2.8.0", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-JbYhLzZD6dHv23bGYusToaOlsdEdMgL/QtKEhwV9fEKgFNoDvkZlak8rTPJUrOlC56QwMOPe1vLG83udlNeVYQ=="],
+ "@react-navigation/native-stack": ["@react-navigation/native-stack@7.6.2", "", { "dependencies": { "@react-navigation/elements": "^2.8.1", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-CB6chGNLwJYiyOeyCNUKx33yT7XJSwRZIeKHf4S1vs+Oqu3u9zMnvGUIsesNgbgX0xy16gBqYsrWgr0ZczBTtA=="],
"@react-navigation/routers": ["@react-navigation/routers@7.5.1", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w=="],
- "@shopify/flash-list": ["@shopify/flash-list@1.8.3", "", { "dependencies": { "recyclerlistview": "4.2.3", "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-vXuj6JyuMjONVOXjEhWFeaONPuWN/53Cl2LeyeM8TZ0JzUcNU+BE6iyga1/yyJeDf0K7YPgAE/PcUX2+DM1LiA=="],
+ "@shopify/flash-list": ["@shopify/flash-list@2.0.2", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w=="],
"@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="],
@@ -587,15 +572,15 @@
"@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="],
- "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
+ "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
"@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="],
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
- "@tanstack/query-core": ["@tanstack/query-core@5.90.5", "", {}, "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.90.7", "", {}, "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ=="],
- "@tanstack/react-query": ["@tanstack/react-query@5.90.5", "", { "dependencies": { "@tanstack/query-core": "5.90.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q=="],
+ "@tanstack/react-query": ["@tanstack/react-query@5.90.7", "", { "dependencies": { "@tanstack/query-core": "5.90.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
@@ -619,13 +604,13 @@
"@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="],
- "@types/jest": ["@types/jest@30.0.0", "", { "dependencies": { "expect": "^30.0.0", "pretty-format": "^30.0.0" } }, "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA=="],
+ "@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
- "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
+ "@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
- "@types/react": ["@types/react@19.0.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw=="],
+ "@types/react": ["@types/react@19.1.17", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA=="],
"@types/react-test-renderer": ["@types/react-test-renderer@19.1.0", "", { "dependencies": { "@types/react": "*" } }, "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ=="],
@@ -635,6 +620,8 @@
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+
"@urql/core": ["@urql/core@5.2.0", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.13", "wonka": "^6.3.2" } }, "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A=="],
"@urql/exchange-retry": ["@urql/exchange-retry@1.3.2", "", { "dependencies": { "@urql/core": "^5.1.2", "wonka": "^6.3.2" } }, "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg=="],
@@ -665,6 +652,8 @@
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
+ "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
+
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
@@ -677,7 +666,7 @@
"anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="],
- "ansi-escapes": ["ansi-escapes@7.1.1", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q=="],
+ "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
"ansi-fragments": ["ansi-fragments@0.2.1", "", { "dependencies": { "colorette": "^1.0.7", "slice-ansi": "^2.0.0", "strip-ansi": "^5.0.0" } }, "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w=="],
@@ -711,7 +700,7 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
- "axios": ["axios@1.13.1", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw=="],
+ "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
"babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="],
@@ -725,15 +714,17 @@
"babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="],
- "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.19.13", "", {}, "sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ=="],
+ "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
- "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.25.1", "", { "dependencies": { "hermes-parser": "0.25.1" } }, "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ=="],
+ "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="],
+
+ "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.29.1", "", { "dependencies": { "hermes-parser": "0.29.1" } }, "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA=="],
"babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="],
"babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="],
- "babel-preset-expo": ["babel-preset-expo@13.2.4", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.79.6", "babel-plugin-react-native-web": "~0.19.13", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "react-refresh": "^0.14.2", "resolve-from": "^5.0.0" }, "peerDependencies": { "babel-plugin-react-compiler": "^19.0.0-beta-e993439-20250405" }, "optionalPeers": ["babel-plugin-react-compiler"] }, "sha512-3IKORo3KR+4qtLdCkZNDj8KeA43oBn7RRQejFGWfiZgu/NeaRUSri8YwYjZqybm7hn3nmMv9OLahlvXBX23o5Q=="],
+ "babel-preset-expo": ["babel-preset-expo@54.0.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.81.5", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo"] }, "sha512-JENWk0bvxW4I1ftveO8GRtX2t2TH6N4Z0TPvIHxroZ/4SswUfyNsUNbbP7Fm4erj3ar/JHGri5kTZ+s3xdjHZw=="],
"babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
@@ -743,7 +734,7 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
- "baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.8.25", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA=="],
"better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="],
@@ -783,10 +774,6 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
- "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="],
-
- "caller-path": ["caller-path@2.0.0", "", { "dependencies": { "caller-callsite": "^2.0.0" } }, "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A=="],
-
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
@@ -795,7 +782,7 @@
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001752", "", {}, "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -905,12 +892,14 @@
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
- "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
+ "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
+
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
@@ -931,7 +920,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.244", "", {}, "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.249", "", {}, "sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -943,7 +932,7 @@
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
- "envinfo": ["envinfo@7.19.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-DoSM9VyG6O3vqBf+p3Gjgr/Q52HYBBtO3v+4koAxt1MnWr+zEnxE+nke/yXS4lt2P4SYCHQ4V3f1i88LQVOpAw=="],
+ "envinfo": ["envinfo@7.20.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
@@ -983,81 +972,83 @@
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
- "expect": ["expect@30.2.0", "", { "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", "jest-matcher-utils": "30.2.0", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw=="],
+ "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="],
- "expo": ["expo@53.0.23", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.24.22", "@expo/config": "~11.0.13", "@expo/config-plugins": "~10.1.2", "@expo/fingerprint": "0.13.4", "@expo/metro-config": "0.20.17", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~13.2.4", "expo-asset": "~11.1.7", "expo-constants": "~17.1.7", "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", "expo-keep-awake": "~14.1.4", "expo-modules-autolinking": "2.1.14", "expo-modules-core": "2.5.0", "react-native-edge-to-edge": "1.6.0", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-6TOLuNCP3AsSkXBJA5W6U/7wpZUop3Q6BxHMtRD2OOgT7CCPvnYgJdnTzqU+gD1hMfcryD8Ejq9RdHbLduXohg=="],
+ "expo": ["expo@54.0.23", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.16", "@expo/config": "~12.0.10", "@expo/config-plugins": "~54.0.2", "@expo/devtools": "0.1.7", "@expo/fingerprint": "0.15.3", "@expo/metro": "~54.1.0", "@expo/metro-config": "54.0.9", "@expo/vector-icons": "^15.0.3", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~54.0.7", "expo-asset": "~12.0.9", "expo-constants": "~18.0.10", "expo-file-system": "~19.0.17", "expo-font": "~14.0.9", "expo-keep-awake": "~15.0.7", "expo-modules-autolinking": "3.0.21", "expo-modules-core": "3.0.25", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-b4uQoiRwQ6nwqsT2709RS15CWYNGF3eJtyr1KyLw9WuMAK7u4jjofkhRiO0+3o1C2NbV+WooyYTOZGubQQMBaQ=="],
- "expo-application": ["expo-application@6.1.5", "", { "peerDependencies": { "expo": "*" } }, "sha512-ToImFmzw8luY043pWFJhh2ZMm4IwxXoHXxNoGdlhD4Ym6+CCmkAvCglg0FK8dMLzAb+/XabmOE7Rbm8KZb6NZg=="],
+ "expo-application": ["expo-application@7.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-Jt1/qqnoDUbZ+bK91+dHaZ1vrPDtRBOltRa681EeedkisqguuEeUx4UHqwVyDK2oHWsK6lO3ojetoA4h8OmNcg=="],
- "expo-asset": ["expo-asset@11.1.7", "", { "dependencies": { "@expo/image-utils": "^0.7.6", "expo-constants": "~17.1.7" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-b5P8GpjUh08fRCf6m5XPVAh7ra42cQrHBIMgH2UXP+xsj4Wufl6pLy6jRF5w6U7DranUMbsXm8TOyq4EHy7ADg=="],
+ "expo-asset": ["expo-asset@12.0.9", "", { "dependencies": { "@expo/image-utils": "^0.8.7", "expo-constants": "~18.0.9" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-vrdRoyhGhBmd0nJcssTSk1Ypx3Mbn/eXaaBCQVkL0MJ8IOZpAObAjfD5CTy8+8RofcHEQdh3wwZVCs7crvfOeg=="],
- "expo-background-task": ["expo-background-task@0.2.8", "", { "dependencies": { "expo-task-manager": "~13.1.6" }, "peerDependencies": { "expo": "*" } }, "sha512-dePyskpmyDZeOtbr9vWFh+Nrse0TvF6YitJqnKcd+3P7pDMiDr1V2aT6zHdNOc5iV9vPaDJoH/zdmlarp1uHMQ=="],
+ "expo-background-task": ["expo-background-task@1.0.8", "", { "dependencies": { "expo-task-manager": "~14.0.7" }, "peerDependencies": { "expo": "*" } }, "sha512-G6WnljBhO0K9j0ntmytF5rZLtYUpwh8n2+hcgmxM1ISPAVVZSPHZhkF9YjBOKpdPWZxmukBgEwejfcGckb8TQQ=="],
- "expo-blur": ["expo-blur@14.1.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-CCLJHxN4eoAl06ESKT3CbMasJ98WsjF9ZQEJnuxtDb9ffrYbZ+g9ru84fukjNUOTtc8A8yXE5z8NgY1l0OMrmQ=="],
+ "expo-blur": ["expo-blur@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-SugQQbQd+zRPy8z2G5qDD4NqhcD7srBF7fN7O7yq6q7ZFK59VWvpDxtMoUkmSfdxgqONsrBN/rLdk00USADrMg=="],
- "expo-brightness": ["expo-brightness@13.1.4", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-we7F+CB/jsDGRcJ1WGvyACTZUcEMx/a1TlwTkDHOORPr31leOwmk6+k98G4i0GrhvdsQt91w3WXnrWyTh7IyJg=="],
+ "expo-brightness": ["expo-brightness@14.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-wccb/NdQEd45UF0lgNEksZt3E8uzlIcxIx1ZqZYWbHyNvcS3LUj5wxB6+ZgKTLeWu4vLQ+oHe+F0QrkC9ojrig=="],
- "expo-build-properties": ["expo-build-properties@0.14.8", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-GTFNZc5HaCS9RmCi6HspCe2+isleuOWt2jh7UEKHTDQ9tdvzkIoWc7U6bQO9lH3Mefk4/BcCUZD/utl7b1wdqw=="],
+ "expo-build-properties": ["expo-build-properties@1.0.9", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-2icttCy3OPTk/GWIFt+vwA+0hup53jnmYb7JKRbvNvrrOrz+WblzpeoiaOleI2dYG/vjwpNO8to8qVyKhYJtrQ=="],
- "expo-constants": ["expo-constants@17.1.7", "", { "dependencies": { "@expo/config": "~11.0.12", "@expo/env": "~1.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA=="],
+ "expo-constants": ["expo-constants@18.0.10", "", { "dependencies": { "@expo/config": "~12.0.10", "@expo/env": "~2.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw=="],
- "expo-dev-client": ["expo-dev-client@5.2.4", "", { "dependencies": { "expo-dev-launcher": "5.1.16", "expo-dev-menu": "6.1.14", "expo-dev-menu-interface": "1.10.0", "expo-manifests": "~0.16.6", "expo-updates-interface": "~1.1.0" }, "peerDependencies": { "expo": "*" } }, "sha512-s/N/nK5LPo0QZJpV4aPijxyrzV4O49S3dN8D2fljqrX2WwFZzWwFO6dX1elPbTmddxumdcpczsdUPY+Ms8g43g=="],
+ "expo-dev-client": ["expo-dev-client@6.0.17", "", { "dependencies": { "expo-dev-launcher": "6.0.17", "expo-dev-menu": "7.0.16", "expo-dev-menu-interface": "2.0.0", "expo-manifests": "~1.0.8", "expo-updates-interface": "~2.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-zVilIum3sqXFbhYhPT6TuxR3ddH/IfHL82FiOTqJUiYaTQqun1I6ogSvU1djhY1eXUYhfYIBieQNWMVjXPxMvw=="],
- "expo-dev-launcher": ["expo-dev-launcher@5.1.16", "", { "dependencies": { "ajv": "8.11.0", "expo-dev-menu": "6.1.14", "expo-manifests": "~0.16.6", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-tbCske9pvbozaEblyxoyo/97D6od9Ma4yAuyUnXtRET1CKAPKYS+c4fiZ+I3B4qtpZwN3JNFUjG3oateN0y6Hg=="],
+ "expo-dev-launcher": ["expo-dev-launcher@6.0.17", "", { "dependencies": { "expo-dev-menu": "7.0.16", "expo-manifests": "~1.0.8" }, "peerDependencies": { "expo": "*" } }, "sha512-riLxFXaw6Nvgb27TiQtUvoHkW/zTz0aO7M+qxDBBaEbJMJSFl51KSwOJJBTItVQIE9f9jB8x5L1CfLw81/McZw=="],
- "expo-dev-menu": ["expo-dev-menu@6.1.14", "", { "dependencies": { "expo-dev-menu-interface": "1.10.0" }, "peerDependencies": { "expo": "*" } }, "sha512-yonNMg2GHJZtuisVowdl1iQjZfYP85r1D1IO+ar9D9zlrBPBJhq2XEju52jd1rDmDkmDuEhBSbPNhzIcsBNiPg=="],
+ "expo-dev-menu": ["expo-dev-menu@7.0.16", "", { "dependencies": { "expo-dev-menu-interface": "2.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-/kjTjk5tcZV0ixYnV3JyzPXKlMimpBNYaDo4XxBbRFIkTf/vmb/9e1BTR2nALnoa/D3MRwtR43gZYT+W/wfKXw=="],
- "expo-dev-menu-interface": ["expo-dev-menu-interface@1.10.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg=="],
+ "expo-dev-menu-interface": ["expo-dev-menu-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw=="],
- "expo-device": ["expo-device@7.1.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-HS04IiE1Fy0FRjBLurr9e5A6yj3kbmQB+2jCZvbSGpsjBnCLdSk/LCii4f5VFhPIBWJLyYuN5QqJyEAw6BcS4Q=="],
+ "expo-device": ["expo-device@8.0.9", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-XqRpaljDNAYZGZzMpC+b9KZfzfydtkwx3pJAp6ODDH+O/5wjAw+mLc5wQMGJCx8/aqVmMsAokec7iebxDPFZDA=="],
"expo-doctor": ["expo-doctor@1.17.11", "", { "bin": { "expo-doctor": "build/index.js" } }, "sha512-4eYZPJm4op2aRQWvd6RA6dZt1mVQQe79n7iqqFi6P927K8w2ld8kZ2D7m/4ahjj9/HBW9NS98m4qGomKJFDuPg=="],
- "expo-file-system": ["expo-file-system@18.1.11", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ=="],
+ "expo-file-system": ["expo-file-system@19.0.17", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-WwaS01SUFrxBnExn87pg0sCTJjZpf2KAOzfImG0o8yhkU7fbYpihpl/oocXBEsNbj58a8hVt1Y4CVV5c1tzu/g=="],
- "expo-font": ["expo-font@13.3.2", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A=="],
+ "expo-font": ["expo-font@14.0.9", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-xCoQbR/36qqB6tew/LQ6GWICpaBmHLhg/Loix5Rku/0ZtNaXMJv08M9o1AcrdiGTn/Xf/BnLu6DgS45cWQEHZg=="],
- "expo-haptics": ["expo-haptics@14.1.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA=="],
+ "expo-haptics": ["expo-haptics@15.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-7flWsYPrwjJxZ8x82RiJtzsnk1Xp9ahnbd9PhCy3NnsemyMApoWIEUr4waPqFr80DtiLZfhD9VMLL1CKa8AImQ=="],
- "expo-image": ["expo-image@2.4.1", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-yHp0Cy4ylOYyLR21CcH6i70DeRyLRPc0yAIPFPn4BT/BpkJNaX5QMXDppcHa58t4WI3Bb8QRJRLuAQaeCtDF8A=="],
+ "expo-image": ["expo-image@3.0.10", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-i4qNCEf9Ur7vDqdfDdFfWnNCAF2efDTdahuDy9iELPS2nzMKBLeeGA2KxYEPuRylGCS96Rwm+SOZJu6INc2ADQ=="],
"expo-json-utils": ["expo-json-utils@0.15.0", "", {}, "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ=="],
- "expo-keep-awake": ["expo-keep-awake@14.1.4", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA=="],
+ "expo-keep-awake": ["expo-keep-awake@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA=="],
- "expo-linear-gradient": ["expo-linear-gradient@14.1.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ=="],
+ "expo-linear-gradient": ["expo-linear-gradient@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-yF+y+9Shpr/OQFfy/wglB/0bykFMbwHBTuMRa5Of/r2P1wbkcacx8rg0JsUWkXH/rn2i2iWdubyqlxSJa3ggZA=="],
- "expo-linking": ["expo-linking@7.1.7", "", { "dependencies": { "expo-constants": "~17.1.7", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ZJaH1RIch2G/M3hx2QJdlrKbYFUTOjVVW4g39hfxrE5bPX9xhZUYXqxqQtzMNl1ylAevw9JkgEfWbBWddbZ3UA=="],
+ "expo-linking": ["expo-linking@8.0.8", "", { "dependencies": { "expo-constants": "~18.0.8", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg=="],
- "expo-localization": ["expo-localization@16.1.6", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-v4HwNzs8QvyKHwl40MvETNEKr77v1o9/eVC8WCBY++DIlBAvonHyJe2R9CfqpZbC4Tlpl7XV+07nLXc8O5PQsA=="],
+ "expo-localization": ["expo-localization@17.0.7", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-ACg1B0tJLNa+f8mZfAaNrMyNzrrzHAARVH1sHHvh+LolKdQpgSKX69Uroz1Llv4C71furpwBklVStbNcEwVVVA=="],
- "expo-manifests": ["expo-manifests@0.16.6", "", { "dependencies": { "@expo/config": "~11.0.12", "expo-json-utils": "~0.15.0" }, "peerDependencies": { "expo": "*" } }, "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w=="],
+ "expo-manifests": ["expo-manifests@1.0.8", "", { "dependencies": { "@expo/config": "~12.0.8", "expo-json-utils": "~0.15.0" }, "peerDependencies": { "expo": "*" } }, "sha512-nA5PwU2uiUd+2nkDWf9e71AuFAtbrb330g/ecvuu52bmaXtN8J8oiilc9BDvAX0gg2fbtOaZdEdjBYopt1jdlQ=="],
- "expo-modules-autolinking": ["expo-modules-autolinking@2.1.14", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "find-up": "^5.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-nT5ERXwc+0ZT/pozDoJjYZyUQu5RnXMk9jDGm5lg+PiKvsrCTSA/2/eftJGMxLkTjVI2MXp5WjSz3JRjbA7UXA=="],
+ "expo-modules-autolinking": ["expo-modules-autolinking@3.0.21", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-pOtPDLln3Ju8DW1zRW4OwZ702YqZ8g+kM/tEY1sWfv22kWUtxkvK+ytRDRpRdnKEnC28okbhWqeMnmVkSFzP6Q=="],
- "expo-modules-core": ["expo-modules-core@2.5.0", "", { "dependencies": { "invariant": "^2.2.4" } }, "sha512-aIbQxZE2vdCKsolQUl6Q9Farlf8tjh/ROR4hfN1qT7QBGPl1XrJGnaOKkcgYaGrlzCPg/7IBe0Np67GzKMZKKQ=="],
+ "expo-modules-core": ["expo-modules-core@3.0.25", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0P8PT8UV6c5/+p8zeVM/FXvBgn/ErtGcMaasqUgbzzBUg94ktbkIrij9t9reGCrir03BYt/Bcpv+EQtYC8JOug=="],
- "expo-notifications": ["expo-notifications@0.31.4", "", { "dependencies": { "@expo/image-utils": "^0.7.6", "@ide/backoff": "^1.0.0", "abort-controller": "^3.0.0", "assert": "^2.0.0", "badgin": "^1.1.5", "expo-application": "~6.1.5", "expo-constants": "~17.1.7" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-NnGKIFGpgZU66qfiFUyjEBYsS77VahURpSSeWEOLt+P1zOaUFlgx2XqS+dxH3/Bn1Vm7TMj04qKsK5KvzR/8Lw=="],
+ "expo-notifications": ["expo-notifications@0.32.12", "", { "dependencies": { "@expo/image-utils": "^0.8.7", "@ide/backoff": "^1.0.0", "abort-controller": "^3.0.0", "assert": "^2.0.0", "badgin": "^1.1.5", "expo-application": "~7.0.7", "expo-constants": "~18.0.9" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-FVJ5W4rOpKvmrLJ1Sd5pxiVTV4a7ApgTlKro+E5X8M2TBbXmEVOjs09klzdalXTjlzmU/Gu8aRw9xr7Ea/gZdw=="],
- "expo-router": ["expo-router@5.1.7", "", { "dependencies": { "@expo/metro-runtime": "5.0.5", "@expo/schema-utils": "^0.1.0", "@expo/server": "^0.6.3", "@radix-ui/react-slot": "1.2.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/native": "^7.1.6", "@react-navigation/native-stack": "^7.3.10", "client-only": "^0.0.1", "invariant": "^2.2.4", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.1.6", "semver": "~7.6.3", "server-only": "^0.0.1", "shallowequal": "^1.1.0" }, "peerDependencies": { "@react-navigation/drawer": "^7.3.9", "expo": "*", "expo-constants": "*", "expo-linking": "*", "react-native-reanimated": "*", "react-native-safe-area-context": "*", "react-native-screens": "*" }, "optionalPeers": ["@react-navigation/drawer", "react-native-reanimated"] }, "sha512-E7hIqTZs4Cub4sbYPeednfYPi+2cyRGMdqc5IYBJ/vC+WBKoYJ8C9eU13ZLbPz//ZybSo2Dsm7v89uFIlO2Gow=="],
+ "expo-router": ["expo-router@6.0.14", "", { "dependencies": { "@expo/metro-runtime": "^6.1.2", "@expo/schema-utils": "^0.1.7", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/native": "^7.1.8", "@react-navigation/native-stack": "^7.3.16", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-server": "^1.0.3", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.1.6", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@react-navigation/drawer": "^7.5.0", "@testing-library/react-native": ">= 12.0.0", "expo": "*", "expo-constants": "^18.0.10", "expo-linking": "^8.0.8", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": ">= 19.0.0" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-vizLO4SgnMEL+PPs2dXr+etEOuksjue7yUQBCtfCEdqoDkQlB0r35zI7rS34Wt53sxKWSlM2p+038qQEpxtiFw=="],
- "expo-screen-orientation": ["expo-screen-orientation@8.1.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-nYwadYtdU6mMDk0MCHMPPPQtBoeFYJ2FspLRW+J35CMLqzE4nbpwGeiImfXzkvD94fpOCfI4KgLj5vGauC3pfA=="],
+ "expo-screen-orientation": ["expo-screen-orientation@9.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-UH/XlB9eMw+I2cyHSkXhAHRAPk83WyA3k5bst7GLu14wRuWiTch9fb6I7qEJK5CN6+XelcWxlBJymys6Fr/FKA=="],
- "expo-sensors": ["expo-sensors@14.1.4", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-KHROi5C8dhXedMwx7fZ5eyv9p382F5XOIex4a+GpdOTL3OY4xyk08kt7x64FtMeeoT87gYD3mb9LrBpHyNubkg=="],
+ "expo-sensors": ["expo-sensors@15.0.7", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-TGUxRx/Ss7KGgfWo453YF64ENucw6oYryPiu/8I3ZZuf114xQPRxAbsZohPLaVUUGuaUyWbDsb0eRsmuKUzBnQ=="],
- "expo-sharing": ["expo-sharing@13.1.5", "", { "peerDependencies": { "expo": "*" } }, "sha512-X/5sAEiWXL2kdoGE3NO5KmbfcmaCWuWVZXHu8OQef7Yig4ZgHFkGD11HKJ5KqDrDg+SRZe4ISd6MxE7vGUgm4w=="],
+ "expo-server": ["expo-server@1.0.4", "", {}, "sha512-IN06r3oPxFh3plSXdvBL7dx0x6k+0/g0bgxJlNISs6qL5Z+gyPuWS750dpTzOeu37KyBG0RcyO9cXUKzjYgd4A=="],
- "expo-splash-screen": ["expo-splash-screen@0.30.10", "", { "dependencies": { "@expo/prebuild-config": "^9.0.10" }, "peerDependencies": { "expo": "*" } }, "sha512-Tt9va/sLENQDQYeOQ6cdLdGvTZ644KR3YG9aRlnpcs2/beYjOX1LHT510EGzVN9ljUTg+1ebEo5GGt2arYtPjw=="],
+ "expo-sharing": ["expo-sharing@14.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-t/5tR8ZJNH6tMkHXlF7453UafNIfrpfTG+THN9EMLC4Wsi4bJuESPm3NdmWDg2D4LDALJI/LQo0iEnLAd5Sp4g=="],
- "expo-status-bar": ["expo-status-bar@2.2.3", "", { "dependencies": { "react-native-edge-to-edge": "1.6.0", "react-native-is-edge-to-edge": "^1.1.6" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+c8R3AESBoduunxTJ8353SqKAKpxL6DvcD8VKBuh81zzJyUUbfB4CVjr1GufSJEKsMzNPXZU+HJwXx7Xh7lx8Q=="],
+ "expo-splash-screen": ["expo-splash-screen@31.0.10", "", { "dependencies": { "@expo/prebuild-config": "^54.0.3" }, "peerDependencies": { "expo": "*" } }, "sha512-i6g9IK798mae4yvflstQ1HkgahIJ6exzTCTw4vEdxV0J2SwiW3Tj+CwRjf0te7Zsb+7dDQhBTmGZwdv00VER2A=="],
- "expo-system-ui": ["expo-system-ui@5.0.11", "", { "dependencies": { "@react-native/normalize-colors": "0.79.6", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-PG5VdaG5cwBe1Rj02mJdnsihKl9Iw/w/a6+qh2mH3f2K/IvQ+Hf7aG2kavSADtkGNCNj7CEIg7Rn4DQz/SE5rQ=="],
+ "expo-status-bar": ["expo-status-bar@3.0.8", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw=="],
- "expo-task-manager": ["expo-task-manager@13.1.6", "", { "dependencies": { "unimodules-app-loader": "~5.1.3" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-sYNAftpIeZ+j6ur17Jo0OpSTk9ks/MDvTbrNCimXMyjIt69XXYL/kAPYf76bWuxOuN8bcJ8Ef8YvihkwFG9hDA=="],
+ "expo-system-ui": ["expo-system-ui@6.0.8", "", { "dependencies": { "@react-native/normalize-colors": "0.81.5", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-DzJYqG2fibBSLzPDL4BybGCiilYOtnI1OWhcYFwoM4k0pnEzMBt1Vj8Z67bXglDDuz2HCQPGNtB3tQft5saKqQ=="],
- "expo-updates-interface": ["expo-updates-interface@1.1.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w=="],
+ "expo-task-manager": ["expo-task-manager@14.0.8", "", { "dependencies": { "unimodules-app-loader": "~6.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HxhyvmulM8px+LQvqIKS85KVx2UodZf5RO+FE2ltpC4mQ5IFkX/ESqiK0grzDa4pVFLyxvs8LjuUKsfB5c39PQ=="],
- "expo-web-browser": ["expo-web-browser@14.2.0", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-6S51d8pVlDRDsgGAp8BPpwnxtyKiMWEFdezNz+5jVIyT+ctReW42uxnjRgtsdn5sXaqzhaX+Tzk/CWaKCyC0hw=="],
+ "expo-updates-interface": ["expo-updates-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg=="],
+
+ "expo-web-browser": ["expo-web-browser@15.0.9", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-Dj8kNFO+oXsxqCDNlUT/GhOrJnm10kAElH++3RplLydogFm5jTzXYWDEeNIDmV+F+BzGYs+sIhxiBf7RyaxXZg=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
@@ -1091,6 +1082,8 @@
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+ "find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="],
+
"flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
@@ -1141,6 +1134,8 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+ "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="],
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -1155,9 +1150,9 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
- "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
+ "hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
- "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+ "hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -1175,7 +1170,7 @@
"hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
- "i18next": ["i18next@25.6.0", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw=="],
+ "i18next": ["i18next@25.6.1", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -1211,8 +1206,6 @@
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
- "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="],
-
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -1237,7 +1230,9 @@
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
- "is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="],
+ "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+
+ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -1249,7 +1244,7 @@
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
- "jest-diff": ["jest-diff@30.2.0", "", { "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "pretty-format": "30.2.0" } }, "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A=="],
+ "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="],
"jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="],
@@ -1257,15 +1252,15 @@
"jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="],
- "jest-matcher-utils": ["jest-matcher-utils@30.2.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.2.0", "pretty-format": "30.2.0" } }, "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg=="],
+ "jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="],
- "jest-message-util": ["jest-message-util@30.2.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw=="],
+ "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="],
- "jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="],
+ "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="],
"jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="],
- "jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="],
+ "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
"jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="],
@@ -1277,7 +1272,7 @@
"joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
- "jotai": ["jotai@2.15.0", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw=="],
+ "jotai": ["jotai@2.15.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-yHT1HAZ3ba2Q8wgaUQ+xfBzEtcS8ie687I8XVCBinfg4bNniyqLIN+utPXWKQE93LMF5fPbQSVRZqgpcN5yd6Q=="],
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
@@ -1289,16 +1284,20 @@
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
- "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="],
-
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+ "json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="],
+
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
+ "jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
+
+ "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
+
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"lan-network": ["lan-network@0.1.7", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ=="],
@@ -1309,27 +1308,29 @@
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
- "lightningcss": ["lightningcss@1.27.0", "", { "dependencies": { "detect-libc": "^1.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.27.0", "lightningcss-darwin-x64": "1.27.0", "lightningcss-freebsd-x64": "1.27.0", "lightningcss-linux-arm-gnueabihf": "1.27.0", "lightningcss-linux-arm64-gnu": "1.27.0", "lightningcss-linux-arm64-musl": "1.27.0", "lightningcss-linux-x64-gnu": "1.27.0", "lightningcss-linux-x64-musl": "1.27.0", "lightningcss-win32-arm64-msvc": "1.27.0", "lightningcss-win32-x64-msvc": "1.27.0" } }, "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ=="],
+ "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
- "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ=="],
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
- "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg=="],
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
- "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA=="],
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
- "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA=="],
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
- "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A=="],
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
- "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg=="],
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
- "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A=="],
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
- "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA=="],
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
- "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ=="],
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
- "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw=="],
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
@@ -1373,33 +1374,33 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
- "metro": ["metro@0.82.5", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.29.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.82.5", "metro-cache": "0.82.5", "metro-cache-key": "0.82.5", "metro-config": "0.82.5", "metro-core": "0.82.5", "metro-file-map": "0.82.5", "metro-resolver": "0.82.5", "metro-runtime": "0.82.5", "metro-source-map": "0.82.5", "metro-symbolicate": "0.82.5", "metro-transform-plugins": "0.82.5", "metro-transform-worker": "0.82.5", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-8oAXxL7do8QckID/WZEKaIFuQJFUTLzfVcC48ghkHhNK2RGuQq8Xvf4AVd+TUA0SZtX0q8TGNXZ/eba1ckeGCg=="],
+ "metro": ["metro@0.83.2", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.2", "metro-cache": "0.83.2", "metro-cache-key": "0.83.2", "metro-config": "0.83.2", "metro-core": "0.83.2", "metro-file-map": "0.83.2", "metro-resolver": "0.83.2", "metro-runtime": "0.83.2", "metro-source-map": "0.83.2", "metro-symbolicate": "0.83.2", "metro-transform-plugins": "0.83.2", "metro-transform-worker": "0.83.2", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-HQgs9H1FyVbRptNSMy/ImchTTE5vS2MSqLoOo7hbDoBq6hPPZokwJvBMwrYSxdjQZmLXz2JFZtdvS+ZfgTc9yw=="],
- "metro-babel-transformer": ["metro-babel-transformer@0.82.5", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.29.1", "nullthrows": "^1.1.1" } }, "sha512-W/scFDnwJXSccJYnOFdGiYr9srhbHPdxX9TvvACOFsIXdLilh3XuxQl/wXW6jEJfgIb0jTvoTlwwrqvuwymr6Q=="],
+ "metro-babel-transformer": ["metro-babel-transformer@0.83.2", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-rirY1QMFlA1uxH3ZiNauBninwTioOgwChnRdDcbB4tgRZ+bGX9DiXoh9QdpppiaVKXdJsII932OwWXGGV4+Nlw=="],
- "metro-cache": ["metro-cache@0.82.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.82.5" } }, "sha512-AwHV9607xZpedu1NQcjUkua8v7HfOTKfftl6Vc9OGr/jbpiJX6Gpy8E/V9jo/U9UuVYX2PqSUcVNZmu+LTm71Q=="],
+ "metro-cache": ["metro-cache@0.83.2", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.2" } }, "sha512-Z43IodutUZeIS7OTH+yQFjc59QlFJ6s5OvM8p2AP9alr0+F8UKr8ADzFzoGKoHefZSKGa4bJx7MZJLF6GwPDHQ=="],
- "metro-cache-key": ["metro-cache-key@0.82.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-qpVmPbDJuRLrT4kcGlUouyqLGssJnbTllVtvIgXfR7ZuzMKf0mGS+8WzcqzNK8+kCyakombQWR0uDd8qhWGJcA=="],
+ "metro-cache-key": ["metro-cache-key@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-3EMG/GkGKYoTaf5RqguGLSWRqGTwO7NQ0qXKmNBjr0y6qD9s3VBXYlwB+MszGtmOKsqE9q3FPrE5Nd9Ipv7rZw=="],
- "metro-config": ["metro-config@0.82.5", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.82.5", "metro-cache": "0.82.5", "metro-core": "0.82.5", "metro-runtime": "0.82.5" } }, "sha512-/r83VqE55l0WsBf8IhNmc/3z71y2zIPe5kRSuqA5tY/SL/ULzlHUJEMd1szztd0G45JozLwjvrhAzhDPJ/Qo/g=="],
+ "metro-config": ["metro-config@0.83.2", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.2", "metro-cache": "0.83.2", "metro-core": "0.83.2", "metro-runtime": "0.83.2", "yaml": "^2.6.1" } }, "sha512-1FjCcdBe3e3D08gSSiU9u3Vtxd7alGH3x/DNFqWDFf5NouX4kLgbVloDDClr1UrLz62c0fHh2Vfr9ecmrOZp+g=="],
- "metro-core": ["metro-core@0.82.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.82.5" } }, "sha512-OJL18VbSw2RgtBm1f2P3J5kb892LCVJqMvslXxuxjAPex8OH7Eb8RBfgEo7VZSjgb/LOf4jhC4UFk5l5tAOHHA=="],
+ "metro-core": ["metro-core@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.2" } }, "sha512-8DRb0O82Br0IW77cNgKMLYWUkx48lWxUkvNUxVISyMkcNwE/9ywf1MYQUE88HaKwSrqne6kFgCSA/UWZoUT0Iw=="],
- "metro-file-map": ["metro-file-map@0.82.5", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ=="],
+ "metro-file-map": ["metro-file-map@0.83.2", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-cMSWnEqZrp/dzZIEd7DEDdk72PXz6w5NOKriJoDN9p1TDQ5nAYrY2lHi8d6mwbcGLoSlWmpPyny9HZYFfPWcGQ=="],
- "metro-minify-terser": ["metro-minify-terser@0.82.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-v6Nx7A4We6PqPu/ta1oGTqJ4Usz0P7c+3XNeBxW9kp8zayS3lHUKR0sY0wsCHInxZlNAEICx791x+uXytFUuwg=="],
+ "metro-minify-terser": ["metro-minify-terser@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-zvIxnh7U0JQ7vT4quasKsijId3dOAWgq+ip2jF/8TMrPUqQabGrs04L2dd0haQJ+PA+d4VvK/bPOY8X/vL2PWw=="],
- "metro-resolver": ["metro-resolver@0.82.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-kFowLnWACt3bEsuVsaRNgwplT8U7kETnaFHaZePlARz4Fg8tZtmRDUmjaD68CGAwc0rwdwNCkWizLYpnyVcs2g=="],
+ "metro-resolver": ["metro-resolver@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Yf5mjyuiRE/Y+KvqfsZxrbHDA15NZxyfg8pIk0qg47LfAJhpMVEX+36e6ZRBq7KVBqy6VDX5Sq55iHGM4xSm7Q=="],
- "metro-runtime": ["metro-runtime@0.82.5", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-rQZDoCUf7k4Broyw3Ixxlq5ieIPiR1ULONdpcYpbJQ6yQ5GGEyYjtkztGD+OhHlw81LCR2SUAoPvtTus2WDK5g=="],
+ "metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="],
- "metro-source-map": ["metro-source-map@0.82.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.82.5", "nullthrows": "^1.1.1", "ob1": "0.82.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-wH+awTOQJVkbhn2SKyaw+0cd+RVSCZ3sHVgyqJFQXIee/yLs3dZqKjjeKKhhVeudgjXo7aE/vSu/zVfcQEcUfw=="],
+ "metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="],
- "metro-symbolicate": ["metro-symbolicate@0.82.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.82.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-1u+07gzrvYDJ/oNXuOG1EXSvXZka/0JSW1q2EYBWerVKMOhvv9JzDGyzmuV7hHbF2Hg3T3S2uiM36sLz1qKsiw=="],
+ "metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="],
- "metro-transform-plugins": ["metro-transform-plugins@0.82.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-57Bqf3rgq9nPqLrT2d9kf/2WVieTFqsQ6qWHpEng5naIUtc/Iiw9+0bfLLWSAw0GH40iJ4yMjFcFJDtNSYynMA=="],
+ "metro-transform-plugins": ["metro-transform-plugins@0.83.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-5WlW25WKPkiJk2yA9d8bMuZrgW7vfA4f4MBb9ZeHbTB3eIAoNN8vS8NENgG/X/90vpTB06X66OBvxhT3nHwP6A=="],
- "metro-transform-worker": ["metro-transform-worker@0.82.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.82.5", "metro-babel-transformer": "0.82.5", "metro-cache": "0.82.5", "metro-cache-key": "0.82.5", "metro-minify-terser": "0.82.5", "metro-source-map": "0.82.5", "metro-transform-plugins": "0.82.5", "nullthrows": "^1.1.1" } }, "sha512-mx0grhAX7xe+XUQH6qoHHlWedI8fhSpDGsfga7CpkO9Lk9W+aPitNtJWNGrW8PfjKEWbT9Uz9O50dkI8bJqigw=="],
+ "metro-transform-worker": ["metro-transform-worker@0.83.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.2", "metro-babel-transformer": "0.83.2", "metro-cache": "0.83.2", "metro-cache-key": "0.83.2", "metro-minify-terser": "0.83.2", "metro-source-map": "0.83.2", "metro-transform-plugins": "0.83.2", "nullthrows": "^1.1.1" } }, "sha512-G5DsIg+cMZ2KNfrdLnWMvtppb3+Rp1GMyj7Bvd9GgYc/8gRmvq1XVEF9XuO87Shhb03kFhGqMTgZerz3hZ1v4Q=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
@@ -1461,7 +1462,7 @@
"nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="],
- "ob1": ["ob1@0.82.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-QyQQ6e66f+Ut/qUVjEce0E/wux5nAGLXYZDn1jr15JWstHsCH3l6VVrg8NKDptW9NEiBXKOJeGF/ydxeSDF3IQ=="],
+ "ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -1485,7 +1486,7 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
- "open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
+ "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
@@ -1507,6 +1508,8 @@
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+ "patch-package": ["patch-package@8.0.1", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^10.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.2.4", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw=="],
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
@@ -1557,9 +1560,11 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
+ "postinstall-postinstall": ["postinstall-postinstall@2.1.0", "", {}, "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ=="],
+
"pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
- "pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
+ "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="],
@@ -1593,11 +1598,11 @@
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
- "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
+ "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
- "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
+ "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
@@ -1607,11 +1612,11 @@
"react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
- "react-native": ["react-native-tvos@0.79.5-0", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native-tvos/virtualized-lists": "0.79.5-0", "@react-native/assets-registry": "0.79.5", "@react-native/codegen": "0.79.5", "@react-native/community-cli-plugin": "0.79.5", "@react-native/gradle-plugin": "0.79.5", "@react-native/js-polyfills": "0.79.5", "@react-native/normalize-colors": "0.79.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.25.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.82.0", "metro-source-map": "^0.82.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.25.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.0.0", "react": "^19.0.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-ayl3M5t+HEib1Kl46QK+VQuFTwY7uAKpLjZlVRlVPMoIVWKa8+gqxM8CeCeaDL+KEbC5cMYxa300fQWG5WJMZQ=="],
+ "react-native": ["react-native-tvos@0.81.5-1", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native-tvos/virtualized-lists": "0.81.5-1", "@react-native/assets-registry": "0.81.5", "@react-native/codegen": "0.81.5", "@react-native/community-cli-plugin": "0.81.5", "@react-native/gradle-plugin": "0.81.5", "@react-native/js-polyfills": "0.81.5", "@react-native/normalize-colors": "0.81.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-jEZ5S8Urjaxkb/pQsfxXslTtKGfeBdaXwEObTyAF3PvCT0wYKD4NbftVJC5Iid9/jKeoBfWTuAOTFfaivqx7IA=="],
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
- "react-native-bottom-tabs": ["react-native-bottom-tabs@0.12.2", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-OXxYtKbJK8hfV7ZrrS/h3vmoB2WOQQRBuV+cqJ1NJSpKxZNBIwHfbUxGlkgcg/I7x2GlmZ9yDDC3KbQ3ouv69Q=="],
+ "react-native-bottom-tabs": ["react-native-bottom-tabs@1.0.2", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-eWNuTpJVefKRaROda4ZeWHvW1cUEb0mw8L7FyLEcPPsd7Tp3rfLRrhptl/O/3mAki9gvpzYE8ASE3GwUrjfp+Q=="],
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
@@ -1621,9 +1626,9 @@
"react-native-device-info": ["react-native-device-info@14.1.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-lXFpe6DJmzbQXNLWxlMHP2xuTU5gwrKAvI8dCAZuERhW9eOXSubOQIesk9lIBnsi9pI19GMrcpJEvs4ARPRYmw=="],
- "react-native-edge-to-edge": ["react-native-edge-to-edge@1.6.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og=="],
+ "react-native-edge-to-edge": ["react-native-edge-to-edge@1.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ=="],
- "react-native-gesture-handler": ["react-native-gesture-handler@2.24.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ZdWyOd1C8axKJHIfYxjJKCcxjWEpUtUWgTOVY2wynbiveSQDm8X/PDyAKXSer/GOtIpjudUbACOndZXCN3vHsw=="],
+ "react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
"react-native-google-cast": ["react-native-google-cast@4.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/HvIKAaWHtG6aTNCxrNrqA2ftWGkfH0M/2iN+28pdGUXpKmueb33mgL1m8D4zzwEODQMcmpfoCsym1IwDvugBQ=="],
@@ -1631,23 +1636,25 @@
"react-native-ios-context-menu": ["react-native-ios-context-menu@3.2.1", "", { "dependencies": { "@dominicstop/ts-event-emitter": "^1.1.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-ios-utilities": "*" } }, "sha512-OBQbb3I/VUx2wQgz4cqN614kt3nJ+qx5wxEdtGN1Aj4nYYL1orp7VLFkV6axof6DgOyv0YD6af2RUTok6a2xDQ=="],
- "react-native-ios-utilities": ["react-native-ios-utilities@5.1.8", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-2lWerAkd0Kn18kUAc/RaBzHnOGG1VjbKVfTR4eEXvwYFYqCS709gOg0tGUaVLsm6CAyMe7/jA+AvKMMztzHf4g=="],
+ "react-native-ios-utilities": ["react-native-ios-utilities@5.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-RTw1Gk8rQhBL43+U80I+Nu8T7mLTNkj5RaG8vTs3ETEDqphS3L0Mrzk79RX0Jmm64HMad70GXHctXFlW1n0V8w=="],
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
- "react-native-mmkv": ["react-native-mmkv@2.12.2", "", { "peerDependencies": { "react": "*", "react-native": ">=0.71.0" } }, "sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg=="],
+ "react-native-mmkv": ["react-native-mmkv@4.0.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-Osoy8as2ZLzO1TTsKxc4tX14Qk19qRVMWnS4ZVBwxie9Re5cjt7rqlpDkJczK3H/y3z70EQ6rmKI/cNMCLGAYQ=="],
+
+ "react-native-nitro-modules": ["react-native-nitro-modules@0.31.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-h/IbVsK5IH7JkvseihAoz/o5dy6CafvGo7j4jTvAa+gnxZWFtXQZg8EDvu0en88LFAumKd/pcF20dzxMiNOmug=="],
"react-native-pager-view": ["react-native-pager-view@6.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw=="],
- "react-native-reanimated": ["react-native-reanimated@3.19.3", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-96ZSE6VRpBAAkrLcnbtCRHvjCqF+jtJCA+in6BN/fu0XUquJAxJHZr8kCt4aHMv6Aqxo6cm9sQEkjMj68JJnZg=="],
+ "react-native-reanimated": ["react-native-reanimated@4.1.3", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*", "react-native-worklets": ">=0.5.0" } }, "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg=="],
"react-native-reanimated-carousel": ["react-native-reanimated-carousel@4.0.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.70.3", "react-native-gesture-handler": ">=2.9.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q=="],
- "react-native-safe-area-context": ["react-native-safe-area-context@5.4.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA=="],
+ "react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="],
- "react-native-screens": ["react-native-screens@4.11.1", "", { "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.1.7", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw=="],
+ "react-native-screens": ["react-native-screens@4.18.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ=="],
- "react-native-svg": ["react-native-svg@15.11.2", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw=="],
+ "react-native-svg": ["react-native-svg@15.12.1", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g=="],
"react-native-tab-view": ["react-native-tab-view@4.2.0", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-TUbh7Yr0tE/99t1pJQLbQ+4/Px67xkT7/r3AhfV+93Q3WoUira0Lx7yuKUP2C118doqxub8NCLERwcqsHr29nQ=="],
@@ -1657,11 +1664,13 @@
"react-native-uuid": ["react-native-uuid@2.0.3", "", {}, "sha512-f/YfIS2f5UB+gut7t/9BKGSCYbRA9/74A5R1MDp+FLYsuS+OSWoiM/D8Jko6OJB6Jcu3v6ONuddvZKHdIGpeiw=="],
- "react-native-video": ["react-native-video@6.14.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-nTPHqg+GKu6UV6Hr5Ph48Hn2jod963bHCt0wWtlK9jv9lE1FDGyDdX0Jl15lky8v7VhNdqjqb9DX0EUhMontWg=="],
+ "react-native-video": ["react-native-video@6.16.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+G6tVVGbwFqNTyPivqb+PhQzWr5OudDQ1dgvBNyBRAgcS8rOcbwuS6oX+m8cxOsXHn1UT9ofQnjQEwkGOsvomg=="],
"react-native-volume-manager": ["react-native-volume-manager@2.0.8", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-aZM47/mYkdQ4CbXpKYO6Ajiczv7fxbQXZ9c0H8gRuQUaS3OCz/MZABer6o9aDWq0KMNsQ7q7GVFLRPnSSeeMmw=="],
- "react-native-web": ["react-native-web@0.20.0", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg=="],
+ "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],
+
+ "react-native-worklets": ["react-native-worklets@0.5.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
@@ -1681,8 +1690,6 @@
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
- "recyclerlistview": ["recyclerlistview@4.2.3", "", { "dependencies": { "lodash.debounce": "4.0.8", "prop-types": "15.8.1", "ts-object-utils": "0.0.5" }, "peerDependencies": { "react": ">= 15.2.1", "react-native": ">= 0.30.0" } }, "sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g=="],
-
"regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="],
"regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="],
@@ -1707,6 +1714,8 @@
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
+ "resolve-global": ["resolve-global@1.0.0", "", { "dependencies": { "global-dirs": "^0.1.1" } }, "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw=="],
+
"resolve-workspace-root": ["resolve-workspace-root@2.0.0", "", {}, "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw=="],
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
@@ -1729,7 +1738,7 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
- "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
+ "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
@@ -1777,7 +1786,7 @@
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
- "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
+ "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="],
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
@@ -1847,7 +1856,7 @@
"terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="],
- "terser": ["terser@5.44.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w=="],
+ "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="],
"test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="],
@@ -1861,6 +1870,8 @@
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
+ "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
+
"tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="],
"to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="],
@@ -1875,8 +1886,6 @@
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
- "ts-object-utils": ["ts-object-utils@0.0.5", "", {}, "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA=="],
-
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
@@ -1885,7 +1894,7 @@
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
- "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="],
@@ -1901,7 +1910,7 @@
"unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="],
- "unimodules-app-loader": ["unimodules-app-loader@5.1.3", "", {}, "sha512-nPUkwfkpJWvdOQrVvyQSUol93/UdmsCVd9Hkx9RgAevmKSVYdZI+S87W73NGKl6QbwK9L1BDSY5OrQuo8Oq15g=="],
+ "unimodules-app-loader": ["unimodules-app-loader@6.0.7", "", {}, "sha512-23iwxmh6/y54PRGJt/xjsOpPK8vlfusBisi3yaVSK22pxg5DmiL/+IHCtbb/crHC+gqdItcy1OoRsZQHfNSBaw=="],
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
@@ -1911,8 +1920,6 @@
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
- "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
-
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-debounce": ["use-debounce@10.0.6", "", { "peerDependencies": { "react": "*" } }, "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg=="],
@@ -1937,6 +1944,8 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+ "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
+
"vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="],
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
@@ -1991,10 +2000,10 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
- "zeego": ["zeego@3.0.6", "", { "dependencies": { "@radix-ui/react-context-menu": "^2.0.1", "@radix-ui/react-dropdown-menu": "^2.0.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "@react-native-menu/menu": "1.2.2", "react": "*", "react-native": "*", "react-native-ios-context-menu": "3.1.0", "react-native-ios-utilities": "5.1.2" } }, "sha512-vg0GCMPYg6or/J91bwRnUpIYwz7QnhkyeKOdd3FjvICg+Gzq2D5QhD8k5RUSv1B+048LpNmNYdLm8qJVIbBONw=="],
-
"zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
+ "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
+
"@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
@@ -2013,8 +2022,6 @@
"@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="],
- "@expo/cli/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
-
"@expo/cli/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"@expo/cli/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -2035,6 +2042,8 @@
"@expo/config-plugins/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
+ "@expo/config-plugins/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
+
"@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"@expo/devcert/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -2053,14 +2062,20 @@
"@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
+ "@expo/mcp-tunnel/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+
+ "@expo/mcp-tunnel/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+ "@expo/metro/metro-runtime": ["metro-runtime@0.83.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-nnsPtgRvFbNKwemqs0FuyFDzXLl+ezuFsUXDbX8o0SXOfsOPijqiQrf3kuafO1Zx1aUWf4NOrKJMAQP5EEHg9A=="],
+
+ "@expo/metro/metro-source-map": ["metro-source-map@0.83.2", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.2", "nullthrows": "^1.1.1", "ob1": "0.83.2", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA=="],
+
"@expo/metro-config/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
"@expo/metro-config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"@expo/metro-config/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
- "@expo/package-manager/@expo/json-file": ["@expo/json-file@10.0.7", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw=="],
-
"@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="],
"@expo/prebuild-config/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
@@ -2079,47 +2094,25 @@
"@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
- "@jest/environment/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="],
-
- "@jest/fake-timers/jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="],
-
- "@jest/fake-timers/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="],
-
- "@jest/fake-timers/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
-
- "@jest/pattern/jest-regex-util": ["jest-regex-util@30.0.1", "", {}, "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA=="],
-
- "@jest/transform/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
-
- "@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
+ "@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"@jimp/png/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
- "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
- "@react-native-community/cli/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
+ "@react-native-community/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
- "@react-native-community/cli-doctor/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
+ "@react-native-community/cli-doctor/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
- "@react-native-community/cli-server-api/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
+ "@react-native-community/cli-server-api/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
- "@react-native-community/cli-tools/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
+ "@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
- "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.79.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ=="],
-
- "@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.79.5", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.79.5", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw=="],
-
- "@react-native/community-cli-plugin/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
-
- "@react-native/community-cli-plugin/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
-
- "@react-native/dev-middleware/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
-
- "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
+ "@react-native/community-cli-plugin/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@react-navigation/bottom-tabs/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
@@ -2139,20 +2132,16 @@
"ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+ "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
+
"babel-preset-expo/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
- "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="],
-
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
- "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
-
- "chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
-
"cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -2167,12 +2156,8 @@
"expo-build-properties/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
- "expo-dev-launcher/ajv": ["ajv@8.11.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg=="],
-
"expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
- "expo-modules-autolinking/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
-
"expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -2201,25 +2186,7 @@
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
- "jest-environment-node/jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="],
-
- "jest-environment-node/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
-
- "jest-haste-map/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
-
- "jest-message-util/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="],
-
- "jest-mock/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="],
-
- "jest-util/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="],
-
- "jest-util/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
-
- "jest-util/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
-
- "jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
-
- "jest-worker/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
+ "jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
@@ -2235,13 +2202,21 @@
"metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
- "metro/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
+ "metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
+
+ "metro/metro-runtime": ["metro-runtime@0.83.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-nnsPtgRvFbNKwemqs0FuyFDzXLl+ezuFsUXDbX8o0SXOfsOPijqiQrf3kuafO1Zx1aUWf4NOrKJMAQP5EEHg9A=="],
+
+ "metro/metro-source-map": ["metro-source-map@0.83.2", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.2", "nullthrows": "^1.1.1", "ob1": "0.83.2", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA=="],
+
+ "metro/metro-symbolicate": ["metro-symbolicate@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.2", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-KoU9BLwxxED6n33KYuQQuc5bXkIxF3fSwlc3ouxrrdLWwhu64muYZNQrukkWzhVKRNFIXW7X2iM8JXpi2heIPw=="],
"metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
- "metro-babel-transformer/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
+ "metro-babel-transformer/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
- "metro-config/cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="],
+ "metro-config/metro-runtime": ["metro-runtime@0.83.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-nnsPtgRvFbNKwemqs0FuyFDzXLl+ezuFsUXDbX8o0SXOfsOPijqiQrf3kuafO1Zx1aUWf4NOrKJMAQP5EEHg9A=="],
+
+ "metro-transform-worker/metro-source-map": ["metro-source-map@0.83.2", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.2", "nullthrows": "^1.1.1", "ob1": "0.83.2", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA=="],
"nativewind/@babel/types": ["@babel/types@7.19.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } }, "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA=="],
@@ -2251,6 +2226,10 @@
"npm-package-arg/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
+ "patch-package/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
+
+ "patch-package/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
+
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -2265,24 +2244,18 @@
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
- "react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
-
- "react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.5", "", {}, "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ=="],
-
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
- "react-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
+ "react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
- "react-native/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
-
- "react-native/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
-
- "react-native-reanimated/react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.1.7", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w=="],
+ "react-native-reanimated/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="],
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
+ "react-native-worklets/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
"readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
"requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="],
@@ -2341,13 +2314,9 @@
"@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
- "@expo/cli/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
+ "@expo/metro/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.2", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-KoU9BLwxxED6n33KYuQQuc5bXkIxF3fSwlc3ouxrrdLWwhu64muYZNQrukkWzhVKRNFIXW7X2iM8JXpi2heIPw=="],
- "@expo/cli/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
-
- "@expo/cli/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
-
- "@expo/package-manager/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
+ "@expo/metro/metro-source-map/ob1": ["ob1@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-XlK3w4M+dwd1g1gvHzVbxiXEbUllRONEgcF2uEO0zm4nxa0eKlh41c6N65q1xbiDOeKKda1tvNOAD33fNjyvCg=="],
"@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
@@ -2367,27 +2336,7 @@
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
- "@jest/environment/jest-mock/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
-
- "@jest/fake-timers/jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
-
- "@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
-
- "@react-native-community/cli-server-api/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
-
- "@react-native-community/cli-server-api/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
-
- "@react-native-community/cli-server-api/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
-
- "@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.79.5", "", {}, "sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A=="],
-
- "@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
-
- "@react-native/community-cli-plugin/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
-
- "@react-native/dev-middleware/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
-
- "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+ "@react-native-community/cli-server-api/open/is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="],
"@react-navigation/bottom-tabs/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -2413,8 +2362,6 @@
"ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
- "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
-
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
@@ -2427,12 +2374,6 @@
"glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
- "jest-validate/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
-
- "jest-validate/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
-
- "jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
-
"lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
@@ -2447,23 +2388,21 @@
"logkitty/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
- "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
+ "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
- "metro-config/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="],
+ "metro-transform-worker/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.2", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-KoU9BLwxxED6n33KYuQQuc5bXkIxF3fSwlc3ouxrrdLWwhu64muYZNQrukkWzhVKRNFIXW7X2iM8JXpi2heIPw=="],
- "metro-config/cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
+ "metro-transform-worker/metro-source-map/ob1": ["ob1@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-XlK3w4M+dwd1g1gvHzVbxiXEbUllRONEgcF2uEO0zm4nxa0eKlh41c6N65q1xbiDOeKKda1tvNOAD33fNjyvCg=="],
- "metro-config/cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="],
+ "metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
- "metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
+ "metro/metro-source-map/ob1": ["ob1@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-XlK3w4M+dwd1g1gvHzVbxiXEbUllRONEgcF2uEO0zm4nxa0eKlh41c6N65q1xbiDOeKKda1tvNOAD33fNjyvCg=="],
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
- "react-native/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
+ "patch-package/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
- "react-native/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
-
- "react-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
+ "patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
@@ -2497,8 +2436,6 @@
"@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
- "@expo/cli/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
-
"@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -2511,16 +2448,6 @@
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
- "@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
-
- "@jest/fake-timers/jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
-
- "@jest/fake-timers/jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
-
- "@react-native-community/cli-server-api/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
-
- "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
-
"@react-navigation/bottom-tabs/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"@react-navigation/bottom-tabs/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -2541,8 +2468,6 @@
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
- "jest-validate/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
-
"log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -2553,12 +2478,6 @@
"logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
- "metro-config/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="],
-
- "metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
-
- "react-native/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
-
"serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
@@ -2577,8 +2496,6 @@
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
- "@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
-
"ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"logkitty/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
diff --git a/components/AddToFavorites.tsx b/components/AddToFavorites.tsx
index a00d20bb..c69a83df 100644
--- a/components/AddToFavorites.tsx
+++ b/components/AddToFavorites.tsx
@@ -1,6 +1,6 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { FC } from "react";
-import { Platform, View, type ViewProps } from "react-native";
+import { View, type ViewProps } from "react-native";
import { RoundButton } from "@/components/RoundButton";
import { useFavorite } from "@/hooks/useFavorite";
@@ -11,24 +11,11 @@ interface Props extends ViewProps {
export const AddToFavorites: FC = ({ item, ...props }) => {
const { isFavorite, toggleFavorite } = useFavorite(item);
- if (Platform.OS === "ios") {
- return (
-
-
-
- );
- }
-
return (
diff --git a/components/AudioTrackSelector.tsx b/components/AudioTrackSelector.tsx
index e8228c86..3a3f9726 100644
--- a/components/AudioTrackSelector.tsx
+++ b/components/AudioTrackSelector.tsx
@@ -1,11 +1,9 @@
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
-import { useMemo } from "react";
-import { Platform, TouchableOpacity, View } from "react-native";
-
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
+import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import { Platform, TouchableOpacity, View } from "react-native";
import { Text } from "./common/Text";
+import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
interface Props extends React.ComponentProps {
source?: MediaSourceInfo;
@@ -20,6 +18,8 @@ export const AudioTrackSelector: React.FC = ({
...props
}) => {
const isTv = Platform.isTV;
+ const [open, setOpen] = useState(false);
+ const { t } = useTranslation();
const audioStreams = useMemo(
() => source?.MediaStreams?.filter((x) => x.Type === "Audio"),
@@ -31,55 +31,58 @@ export const AudioTrackSelector: React.FC = ({
[audioStreams, selected],
);
- const { t } = useTranslation();
+ const optionGroups: OptionGroup[] = useMemo(
+ () => [
+ {
+ options:
+ audioStreams?.map((audio, idx) => ({
+ type: "radio" as const,
+ label: audio.DisplayTitle || `Audio Stream ${idx + 1}`,
+ value: audio.Index ?? idx,
+ selected: audio.Index === selected,
+ onPress: () => {
+ if (audio.Index !== null && audio.Index !== undefined) {
+ onChange(audio.Index);
+ }
+ },
+ })) || [],
+ },
+ ],
+ [audioStreams, selected, onChange],
+ );
+
+ const handleOptionSelect = () => {
+ setOpen(false);
+ };
+
+ const trigger = (
+
+ {t("item_card.audio")}
+ setOpen(true)}
+ >
+ {selectedAudioSteam?.DisplayTitle}
+
+
+ );
if (isTv) return null;
return (
-
-
-
-
-
- {t("item_card.audio")}
-
-
-
- {selectedAudioSteam?.DisplayTitle}
-
-
-
-
-
- Audio streams
- {audioStreams?.map((audio, idx: number) => (
- {
- if (audio.Index !== null && audio.Index !== undefined)
- onChange(audio.Index);
- }}
- >
-
- {audio.DisplayTitle}
-
-
- ))}
-
-
-
+ bottomSheetConfig={{
+ enablePanDownToClose: true,
+ }}
+ />
);
};
diff --git a/components/BitrateSelector.tsx b/components/BitrateSelector.tsx
index d52a6bf6..26d6e2c4 100644
--- a/components/BitrateSelector.tsx
+++ b/components/BitrateSelector.tsx
@@ -1,10 +1,8 @@
-import { Platform, TouchableOpacity, View } from "react-native";
-
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import { Platform, TouchableOpacity, View } from "react-native";
import { Text } from "./common/Text";
+import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
export type Bitrate = {
key: string;
@@ -61,6 +59,8 @@ export const BitrateSelector: React.FC = ({
...props
}) => {
const isTv = Platform.isTV;
+ const [open, setOpen] = useState(false);
+ const { t } = useTranslation();
const sorted = useMemo(() => {
if (inverted)
@@ -76,53 +76,59 @@ export const BitrateSelector: React.FC = ({
);
}, [inverted]);
- const { t } = useTranslation();
+ const optionGroups: OptionGroup[] = useMemo(
+ () => [
+ {
+ options: sorted.map((bitrate) => ({
+ type: "radio" as const,
+ label: bitrate.key,
+ value: bitrate,
+ selected: bitrate.value === selected?.value,
+ onPress: () => onChange(bitrate),
+ })),
+ },
+ ],
+ [sorted, selected, onChange],
+ );
+
+ const handleOptionSelect = (optionId: string) => {
+ const selectedBitrate = sorted.find((b) => b.key === optionId);
+ if (selectedBitrate) {
+ onChange(selectedBitrate);
+ }
+ setOpen(false);
+ };
+
+ const trigger = (
+
+ {t("item_card.quality")}
+ setOpen(true)}
+ >
+
+ {BITRATES.find((b) => b.value === selected?.value)?.key}
+
+
+
+ );
if (isTv) return null;
return (
-
-
-
-
-
- {t("item_card.quality")}
-
-
-
- {BITRATES.find((b) => b.value === selected?.value)?.key}
-
-
-
-
-
- Bitrates
- {sorted.map((b) => (
- {
- onChange(b);
- }}
- >
- {b.key}
-
- ))}
-
-
-
+ bottomSheetConfig={{
+ enablePanDownToClose: true,
+ }}
+ />
);
};
diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx
index 80ead77a..fe34d7c3 100644
--- a/components/DownloadItem.tsx
+++ b/components/DownloadItem.tsx
@@ -64,9 +64,8 @@ export const DownloadItems: React.FC = ({
const { settings } = useSettings();
const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
- const { processes, startBackgroundDownload, getDownloadedItems } =
- useDownload();
- const downloadedFiles = getDownloadedItems();
+ const { processes, startBackgroundDownload, downloadedItems } = useDownload();
+ const downloadedFiles = downloadedItems;
const [selectedOptions, setSelectedOptions] = useState<
SelectedOptions | undefined
@@ -90,11 +89,8 @@ export const DownloadItems: React.FC = ({
bottomSheetModalRef.current?.present();
}, []);
- const handleSheetChanges = useCallback((index: number) => {
- // Ensure modal is fully dismissed when index is -1
- if (index === -1) {
- // Modal is fully closed
- }
+ const handleSheetChanges = useCallback((_index: number) => {
+ // Modal state tracking handled by BottomSheetModal
}, []);
const closeModal = useCallback(() => {
@@ -157,6 +153,13 @@ export const DownloadItems: React.FC = ({
itemsNotDownloaded.every((p) => queue.some((q) => p.Id === q.item.Id))
);
}, [queue, itemsNotDownloaded]);
+
+ const itemsInProgressOrQueued = useMemo(() => {
+ const inProgress = itemsProcesses.length;
+ const inQueue = queue.filter((q) => itemIds.includes(q.item.Id)).length;
+ return inProgress + inQueue;
+ }, [itemsProcesses, queue, itemIds]);
+
const navigateToDownloads = () => router.push("/downloads");
const onDownloadedPress = () => {
@@ -256,13 +259,12 @@ export const DownloadItems: React.FC = ({
throw new Error("No item id");
}
- // Ensure modal is dismissed before starting download
- await closeModal();
+ closeModal();
- // Small delay to ensure modal is fully dismissed
- setTimeout(() => {
+ // Wait for modal dismiss animation to complete
+ requestAnimationFrame(() => {
initiateDownload(...itemsToDownload);
- }, 100);
+ });
} else {
toast.error(
t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
@@ -282,7 +284,14 @@ export const DownloadItems: React.FC = ({
);
const renderButtonContent = () => {
- if (processes.length > 0 && itemsProcesses.length > 0) {
+ // For single item downloads, show progress if item is being processed
+ // For multi-item downloads (season/series), show progress only if 2+ items are in progress or queued
+ const shouldShowProgress =
+ itemIds.length === 1
+ ? itemsProcesses.length > 0
+ : itemsInProgressOrQueued > 1;
+
+ if (processes.length > 0 && shouldShowProgress) {
return progress === 0 ? (
) : (
@@ -336,9 +345,6 @@ export const DownloadItems: React.FC = ({
backgroundColor: "#171717",
}}
onChange={handleSheetChanges}
- onDismiss={() => {
- // Ensure any pending state is cleared when modal is dismissed
- }}
backdropComponent={renderBackdrop}
enablePanDownToClose
enableDismissOnClose
@@ -359,16 +365,18 @@ export const DownloadItems: React.FC = ({
})}
-
-
- setSelectedOptions(
- (prev) => prev && { ...prev, bitrate: val },
- )
- }
- selected={selectedOptions?.bitrate}
- />
+
+
+
+ setSelectedOptions(
+ (prev) => prev && { ...prev, bitrate: val },
+ )
+ }
+ selected={selectedOptions?.bitrate}
+ />
+
{itemsNotDownloaded.length > 1 && (
{t("item_card.download.download_unwatched_only")}
@@ -380,21 +388,23 @@ export const DownloadItems: React.FC = ({
)}
{itemsNotDownloaded.length === 1 && (
-
- setSelectedOptions(
- (prev) =>
- prev && {
- ...prev,
- mediaSource: val,
- },
- )
- }
- selected={selectedOptions?.mediaSource}
- />
+
+
+ setSelectedOptions(
+ (prev) =>
+ prev && {
+ ...prev,
+ mediaSource: val,
+ },
+ )
+ }
+ selected={selectedOptions?.mediaSource}
+ />
+
{selectedOptions?.mediaSource && (
-
+
{
@@ -427,11 +437,7 @@ export const DownloadItems: React.FC = ({
)}
-
diff --git a/components/ExampleGlobalModalUsage.tsx b/components/ExampleGlobalModalUsage.tsx
new file mode 100644
index 00000000..ccebb823
--- /dev/null
+++ b/components/ExampleGlobalModalUsage.tsx
@@ -0,0 +1,203 @@
+/**
+ * Example Usage of Global Modal
+ *
+ * This file demonstrates how to use the global modal system from anywhere in your app.
+ * You can delete this file after understanding how it works.
+ */
+
+import { Ionicons } from "@expo/vector-icons";
+import { TouchableOpacity, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useGlobalModal } from "@/providers/GlobalModalProvider";
+
+/**
+ * Example 1: Simple Content Modal
+ */
+export const SimpleModalExample = () => {
+ const { showModal } = useGlobalModal();
+
+ const handleOpenModal = () => {
+ showModal(
+
+ Simple Modal
+
+ This is a simple modal with just some text content.
+
+
+ Swipe down or tap outside to close.
+
+ ,
+ );
+ };
+
+ return (
+
+ Open Simple Modal
+
+ );
+};
+
+/**
+ * Example 2: Modal with Custom Snap Points
+ */
+export const CustomSnapPointsExample = () => {
+ const { showModal } = useGlobalModal();
+
+ const handleOpenModal = () => {
+ showModal(
+
+
+ Custom Snap Points
+
+
+ This modal has custom snap points (25%, 50%, 90%).
+
+
+
+ Try dragging the modal to different heights!
+
+
+ ,
+ {
+ snapPoints: ["25%", "50%", "90%"],
+ enableDynamicSizing: false,
+ },
+ );
+ };
+
+ return (
+
+ Custom Snap Points
+
+ );
+};
+
+/**
+ * Example 3: Complex Component in Modal
+ */
+const SettingsModalContent = () => {
+ const { hideModal } = useGlobalModal();
+
+ const settings = [
+ {
+ id: 1,
+ title: "Notifications",
+ icon: "notifications-outline" as const,
+ enabled: true,
+ },
+ { id: 2, title: "Dark Mode", icon: "moon-outline" as const, enabled: true },
+ {
+ id: 3,
+ title: "Auto-play",
+ icon: "play-outline" as const,
+ enabled: false,
+ },
+ ];
+
+ return (
+
+ Settings
+
+ {settings.map((setting, index) => (
+
+
+
+ {setting.title}
+
+
+
+
+
+ ))}
+
+
+ Close
+
+
+ );
+};
+
+export const ComplexModalExample = () => {
+ const { showModal } = useGlobalModal();
+
+ const handleOpenModal = () => {
+ showModal();
+ };
+
+ return (
+
+ Complex Component
+
+ );
+};
+
+/**
+ * Example 4: Modal Triggered from Function (e.g., API response)
+ */
+export const useShowSuccessModal = () => {
+ const { showModal } = useGlobalModal();
+
+ return (message: string) => {
+ showModal(
+
+
+
+
+ Success!
+ {message}
+ ,
+ );
+ };
+};
+
+/**
+ * Main Demo Component
+ */
+export const GlobalModalDemo = () => {
+ const showSuccess = useShowSuccessModal();
+
+ return (
+
+
+ Global Modal Examples
+
+
+
+
+
+
+ showSuccess("Operation completed successfully!")}
+ className='bg-orange-600 px-4 py-2 rounded-lg'
+ >
+ Show Success Modal
+
+
+ );
+};
diff --git a/components/GlobalModal.tsx b/components/GlobalModal.tsx
new file mode 100644
index 00000000..361321d3
--- /dev/null
+++ b/components/GlobalModal.tsx
@@ -0,0 +1,73 @@
+import {
+ BottomSheetBackdrop,
+ type BottomSheetBackdropProps,
+ BottomSheetModal,
+} from "@gorhom/bottom-sheet";
+import { useCallback } from "react";
+import { useGlobalModal } from "@/providers/GlobalModalProvider";
+
+/**
+ * GlobalModal Component
+ *
+ * This component renders a global bottom sheet modal that can be controlled
+ * from anywhere in the app using the useGlobalModal hook.
+ *
+ * Place this component at the root level of your app (in _layout.tsx)
+ * after BottomSheetModalProvider.
+ */
+export const GlobalModal = () => {
+ const { hideModal, modalState, modalRef } = useGlobalModal();
+
+ const handleSheetChanges = useCallback(
+ (index: number) => {
+ if (index === -1) {
+ hideModal();
+ }
+ },
+ [hideModal],
+ );
+
+ const renderBackdrop = useCallback(
+ (props: BottomSheetBackdropProps) => (
+
+ ),
+ [],
+ );
+
+ const defaultOptions = {
+ enableDynamicSizing: true,
+ enablePanDownToClose: true,
+ backgroundStyle: {
+ backgroundColor: "#171717",
+ },
+ handleIndicatorStyle: {
+ backgroundColor: "white",
+ },
+ };
+
+ // Merge default options with provided options
+ const modalOptions = { ...defaultOptions, ...modalState.options };
+
+ return (
+
+ {modalState.content}
+
+ );
+};
diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx
index 070b02e2..d66a7e5c 100644
--- a/components/ItemContent.tsx
+++ b/components/ItemContent.tsx
@@ -204,7 +204,7 @@ export const ItemContent: React.FC = React.memo(
{item.Type !== "Program" && !Platform.isTV && !isOffline && (
-
+
diff --git a/components/MediaSourceSelector.tsx b/components/MediaSourceSelector.tsx
index 3125f654..70f397f6 100644
--- a/components/MediaSourceSelector.tsx
+++ b/components/MediaSourceSelector.tsx
@@ -2,13 +2,11 @@ import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
-import { useCallback, useMemo } from "react";
-import { Platform, TouchableOpacity, View } from "react-native";
-
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
+import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import { Platform, TouchableOpacity, View } from "react-native";
import { Text } from "./common/Text";
+import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
interface Props extends React.ComponentProps {
item: BaseItemDto;
@@ -23,7 +21,7 @@ export const MediaSourceSelector: React.FC = ({
...props
}) => {
const isTv = Platform.isTV;
-
+ const [open, setOpen] = useState(false);
const { t } = useTranslation();
const getDisplayName = useCallback((source: MediaSourceInfo) => {
@@ -46,50 +44,60 @@ export const MediaSourceSelector: React.FC = ({
return getDisplayName(selected);
}, [selected, getDisplayName]);
+ const optionGroups: OptionGroup[] = useMemo(
+ () => [
+ {
+ options:
+ item.MediaSources?.map((source) => ({
+ type: "radio" as const,
+ label: getDisplayName(source),
+ value: source,
+ selected: source.Id === selected?.Id,
+ onPress: () => onChange(source),
+ })) || [],
+ },
+ ],
+ [item.MediaSources, selected, getDisplayName, onChange],
+ );
+
+ const handleOptionSelect = (optionId: string) => {
+ const selectedSource = item.MediaSources?.find(
+ (source, idx) => `${source.Id || idx}` === optionId,
+ );
+ if (selectedSource) {
+ onChange(selectedSource);
+ }
+ setOpen(false);
+ };
+
+ const trigger = (
+
+ {t("item_card.video")}
+ setOpen(true)}
+ >
+ {selectedName}
+
+
+ );
+
if (isTv) return null;
return (
-
-
-
-
-
- {t("item_card.video")}
-
-
- {selectedName}
-
-
-
-
- Media sources
- {item.MediaSources?.map((source, idx: number) => (
- {
- onChange(source);
- }}
- >
-
- {getDisplayName(source)}
-
-
- ))}
-
-
-
+ bottomSheetConfig={{
+ enablePanDownToClose: true,
+ }}
+ />
);
};
diff --git a/components/ParallaxPage.tsx b/components/ParallaxPage.tsx
index 21b0540a..2f5d9adb 100644
--- a/components/ParallaxPage.tsx
+++ b/components/ParallaxPage.tsx
@@ -99,7 +99,7 @@ export const ParallaxScrollView: React.FC> = ({
style={{
top: -50,
}}
- className='relative flex-1 bg-transparent pb-24'
+ className='relative flex-1 bg-transparent pb-4'
>
= {
+ type: "radio";
+ label: string;
+ value: T;
+ selected: boolean;
+ onPress: () => void;
+ disabled?: boolean;
+};
+
+export type ToggleOption = {
+ type: "toggle";
+ label: string;
+ value: boolean;
+ onToggle: () => void;
+ disabled?: boolean;
+};
+
+export type Option = RadioOption | ToggleOption;
+
+// Option group structure
+export type OptionGroup = {
+ title?: string;
+ options: Option[];
+};
+
+interface PlatformDropdownProps {
+ trigger?: React.ReactNode;
+ title?: string;
+ groups: OptionGroup[];
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ onOptionSelect?: (value?: any) => void;
+ expoUIConfig?: {
+ hostStyle?: any;
+ };
+ bottomSheetConfig?: {
+ enableDynamicSizing?: boolean;
+ enablePanDownToClose?: boolean;
+ };
+}
+
+const ToggleSwitch: React.FC<{ value: boolean }> = ({ value }) => (
+
+
+
+);
+
+const OptionItem: React.FC<{ option: Option; isLast?: boolean }> = ({
+ option,
+ isLast,
+}) => {
+ const isToggle = option.type === "toggle";
+ const handlePress = isToggle ? option.onToggle : option.onPress;
+
+ return (
+ <>
+
+ {option.label}
+ {isToggle ? (
+
+ ) : option.selected ? (
+
+ ) : (
+
+ )}
+
+ {!isLast && (
+
+ )}
+ >
+ );
+};
+
+const OptionGroupComponent: React.FC<{ group: OptionGroup }> = ({ group }) => (
+
+ {group.title && (
+
+ {group.title}
+
+ )}
+
+ {group.options.map((option, index) => (
+
+ ))}
+
+
+);
+
+const BottomSheetContent: React.FC<{
+ title?: string;
+ groups: OptionGroup[];
+ onOptionSelect?: (value?: any) => void;
+ onClose?: () => void;
+}> = ({ title, groups, onOptionSelect, onClose }) => {
+ const insets = useSafeAreaInsets();
+
+ // Wrap the groups to call onOptionSelect when an option is pressed
+ const wrappedGroups = groups.map((group) => ({
+ ...group,
+ options: group.options.map((option) => {
+ if (option.type === "radio") {
+ return {
+ ...option,
+ onPress: () => {
+ option.onPress();
+ onOptionSelect?.(option.value);
+ onClose?.();
+ },
+ };
+ }
+ if (option.type === "toggle") {
+ return {
+ ...option,
+ onToggle: () => {
+ option.onToggle();
+ onOptionSelect?.(option.value);
+ },
+ };
+ }
+ return option;
+ }),
+ }));
+
+ return (
+
+ {title && {title}}
+ {wrappedGroups.map((group, index) => (
+
+ ))}
+
+ );
+};
+
+const PlatformDropdownComponent = ({
+ trigger,
+ title,
+ groups,
+ open: controlledOpen,
+ onOpenChange: controlledOnOpenChange,
+ onOptionSelect,
+ expoUIConfig,
+ bottomSheetConfig,
+}: PlatformDropdownProps) => {
+ const { showModal, hideModal } = useGlobalModal();
+
+ // Use internal state if not controlled externally
+ const [internalOpen, setInternalOpen] = useState(false);
+ const open = controlledOpen ?? internalOpen;
+ const onOpenChange = controlledOnOpenChange ?? setInternalOpen;
+
+ // Handle open/close state changes for Android
+ useEffect(() => {
+ if (Platform.OS === "android" && open === true) {
+ showModal(
+ {
+ hideModal();
+ onOpenChange?.(false);
+ }}
+ />,
+ {
+ snapPoints: ["90%"],
+ enablePanDownToClose: bottomSheetConfig?.enablePanDownToClose ?? true,
+ },
+ );
+ }
+ }, [
+ open,
+ title,
+ groups,
+ onOptionSelect,
+ onOpenChange,
+ bottomSheetConfig,
+ showModal,
+ hideModal,
+ ]);
+
+ if (Platform.OS === "ios") {
+ return (
+
+
+
+
+ {trigger || Show Menu}
+
+
+
+ {groups.flatMap((group, groupIndex) => {
+ // Check if this group has radio options
+ const radioOptions = group.options.filter(
+ (opt) => opt.type === "radio",
+ ) as RadioOption[];
+ const toggleOptions = group.options.filter(
+ (opt) => opt.type === "toggle",
+ ) as ToggleOption[];
+
+ const items = [];
+
+ // Add Picker for radio options ONLY if there's a group title
+ // Otherwise render as individual buttons
+ if (radioOptions.length > 0) {
+ if (group.title) {
+ // Use Picker for grouped options
+ items.push(
+ 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(
+ {
+ option.onPress();
+ onOptionSelect?.(option.value);
+ }}
+ disabled={option.disabled}
+ >
+ {option.label}
+ ,
+ );
+ });
+ }
+ }
+
+ // Add Buttons for toggle options
+ toggleOptions.forEach((option, optionIndex) => {
+ items.push(
+ {
+ option.onToggle();
+ onOptionSelect?.(option.value);
+ }}
+ disabled={option.disabled}
+ >
+ {option.label}
+ ,
+ );
+ });
+
+ return items;
+ })}
+
+
+
+ );
+ }
+
+ // Android: Wrap trigger in TouchableOpacity to handle press events
+ // The useEffect above watches for open state changes and shows/hides the modal
+ return (
+ onOpenChange(true)} activeOpacity={0.7}>
+ {trigger || Open Menu}
+
+ );
+};
+
+// Memoize to prevent unnecessary re-renders when parent re-renders
+export const PlatformDropdown = React.memo(
+ PlatformDropdownComponent,
+ (prevProps, nextProps) => {
+ // Custom comparison - only re-render if these props actually change
+ return (
+ prevProps.title === nextProps.title &&
+ prevProps.open === nextProps.open &&
+ prevProps.groups === nextProps.groups && // Reference equality (works because we memoize groups in caller)
+ prevProps.trigger === nextProps.trigger // Reference equality
+ );
+ },
+);
diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx
index 6ac1956e..62e626db 100644
--- a/components/PlayButton.tsx
+++ b/components/PlayButton.tsx
@@ -1,11 +1,12 @@
import { useActionSheet } from "@expo/react-native-action-sheet";
+import { Button, Host } from "@expo/ui/swift-ui";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useRouter } from "expo-router";
import { useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
-import { Alert, TouchableOpacity, View } from "react-native";
+import { Alert, Platform, TouchableOpacity, View } from "react-native";
import CastContext, {
CastButton,
PlayServicesState,
@@ -33,10 +34,9 @@ import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { chromecast } from "@/utils/profiles/chromecast";
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
import { runtimeTicksToMinutes } from "@/utils/time";
-import type { Button } from "./Button";
import type { SelectedOptions } from "./ItemContent";
-interface Props extends React.ComponentProps {
+interface Props extends React.ComponentProps {
item: BaseItemDto;
selectedOptions: SelectedOptions;
isOffline?: boolean;
@@ -165,7 +165,7 @@ export const PlayButton: React.FC = ({
api,
item,
deviceProfile: enableH265 ? chromecasth265 : chromecast,
- startTimeTicks: item?.UserData?.PlaybackPositionTicks!,
+ startTimeTicks: item?.UserData?.PlaybackPositionTicks ?? 0,
userId: user.Id,
audioStreamIndex: selectedOptions.audioIndex,
maxStreamingBitrate: selectedOptions.bitrate?.value,
@@ -364,6 +364,46 @@ export const PlayButton: React.FC = ({
* *********************
*/
+ if (Platform.OS === "ios")
+ return (
+
+
+
+
+ {runtimeTicksToMinutes(item?.RunTimeTicks)}
+
+
+
+
+ {client && (
+
+
+
+
+ )}
+ {!client && settings?.openInVLC && (
+
+
+
+ )}
+
+
+
+ );
+
return (
= ({
accessibilityHint='Tap to play the media'
onPress={onPress}
className={"relative"}
- {...props}
>
= ({ items, ...props }) => {
const allPlayed = items.every((item) => item.UserData?.Played);
const toggle = useMarkAsPlayed(items);
- if (Platform.OS === "ios") {
- return (
-
- {
- await toggle(!allPlayed);
- }}
- size={props.size}
- />
-
- );
- }
-
return (
{
await toggle(!allPlayed);
diff --git a/components/RoundButton.tsx b/components/RoundButton.tsx
index caa86c04..5d490f19 100644
--- a/components/RoundButton.tsx
+++ b/components/RoundButton.tsx
@@ -96,7 +96,7 @@ export const RoundButton: React.FC> = ({
diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx
index de3f4697..6fca1955 100644
--- a/components/SubtitleTrackSelector.tsx
+++ b/components/SubtitleTrackSelector.tsx
@@ -1,12 +1,10 @@
import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native";
import { tc } from "@/utils/textTools";
-
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
-import { useTranslation } from "react-i18next";
import { Text } from "./common/Text";
+import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
interface Props extends React.ComponentProps {
source?: MediaSourceInfo;
@@ -21,6 +19,8 @@ export const SubtitleTrackSelector: React.FC = ({
...props
}) => {
const { t } = useTranslation();
+ const [open, setOpen] = useState(false);
+
const subtitleStreams = useMemo(() => {
return source?.MediaStreams?.filter((x) => x.Type === "Subtitle");
}, [source]);
@@ -30,64 +30,83 @@ export const SubtitleTrackSelector: React.FC = ({
[subtitleStreams, selected],
);
+ const optionGroups: OptionGroup[] = useMemo(() => {
+ const options = [
+ {
+ type: "radio" as const,
+ label: t("item_card.none"),
+ value: -1,
+ selected: selected === -1,
+ onPress: () => onChange(-1),
+ },
+ ...(subtitleStreams?.map((subtitle, idx) => ({
+ type: "radio" as const,
+ label: subtitle.DisplayTitle || `Subtitle Stream ${idx + 1}`,
+ value: subtitle.Index,
+ selected: subtitle.Index === selected,
+ onPress: () => onChange(subtitle.Index ?? -1),
+ })) || []),
+ ];
+
+ return [
+ {
+ options,
+ },
+ ];
+ }, [subtitleStreams, selected, t, onChange]);
+
+ const handleOptionSelect = (optionId: string) => {
+ if (optionId === "none") {
+ onChange(-1);
+ } else {
+ const selectedStream = subtitleStreams?.find(
+ (subtitle, idx) => `${subtitle.Index || idx}` === optionId,
+ );
+ if (
+ selectedStream &&
+ selectedStream.Index !== undefined &&
+ selectedStream.Index !== null
+ ) {
+ onChange(selectedStream.Index);
+ }
+ }
+ setOpen(false);
+ };
+
+ const trigger = (
+
+
+ {t("item_card.subtitles")}
+
+ setOpen(true)}
+ >
+
+ {selectedSubtitleSteam
+ ? tc(selectedSubtitleSteam?.DisplayTitle, 7)
+ : t("item_card.none")}
+
+
+
+ );
+
if (Platform.isTV || subtitleStreams?.length === 0) return null;
return (
-
-
-
-
-
- {t("item_card.subtitles")}
-
-
-
- {selectedSubtitleSteam
- ? tc(selectedSubtitleSteam?.DisplayTitle, 7)
- : t("item_card.none")}
-
-
-
-
-
- Subtitle tracks
- {
- onChange(-1);
- }}
- >
- None
-
- {subtitleStreams?.map((subtitle, idx: number) => (
- {
- if (subtitle.Index !== undefined && subtitle.Index !== null)
- onChange(subtitle.Index);
- }}
- >
-
- {subtitle.DisplayTitle}
-
-
- ))}
-
-
-
+ bottomSheetConfig={{
+ enablePanDownToClose: true,
+ }}
+ />
);
};
diff --git a/components/AppleTVCarousel.tsx b/components/apple-tv-carousel/AppleTVCarousel.tsx
similarity index 87%
rename from components/AppleTVCarousel.tsx
rename to components/apple-tv-carousel/AppleTVCarousel.tsx
index 4f9ee1c3..c30711e8 100644
--- a/components/AppleTVCarousel.tsx
+++ b/components/apple-tv-carousel/AppleTVCarousel.tsx
@@ -28,15 +28,16 @@ import Animated, {
} from "react-native-reanimated";
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
+import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
-import { ItemImage } from "./common/ItemImage";
-import { getItemNavigation } from "./common/TouchableItemRouter";
-import type { SelectedOptions } from "./ItemContent";
-import { PlayButton } from "./PlayButton";
-import { PlayedStatus } from "./PlayedStatus";
+import { ItemImage } from "../common/ItemImage";
+import { getItemNavigation } from "../common/TouchableItemRouter";
+import type { SelectedOptions } from "../ItemContent";
+import { PlayButton } from "../PlayButton";
+import { MarkAsPlayedLargeButton } from "./MarkAsPlayedLargeButton";
interface AppleTVCarouselProps {
initialIndex?: number;
@@ -50,10 +51,11 @@ const GRADIENT_HEIGHT_BOTTOM = 150;
const LOGO_HEIGHT = 80;
// Position Constants
-const LOGO_BOTTOM_POSITION = 210;
-const GENRES_BOTTOM_POSITION = 170;
-const CONTROLS_BOTTOM_POSITION = 100;
-const DOTS_BOTTOM_POSITION = 60;
+const LOGO_BOTTOM_POSITION = 260;
+const GENRES_BOTTOM_POSITION = 220;
+const OVERVIEW_BOTTOM_POSITION = 165;
+const CONTROLS_BOTTOM_POSITION = 80;
+const DOTS_BOTTOM_POSITION = 40;
// Size Constants
const DOT_HEIGHT = 6;
@@ -63,13 +65,15 @@ const PLAY_BUTTON_SKELETON_HEIGHT = 50;
const PLAYED_STATUS_SKELETON_SIZE = 40;
const TEXT_SKELETON_HEIGHT = 20;
const TEXT_SKELETON_WIDTH = 250;
+const OVERVIEW_SKELETON_HEIGHT = 16;
+const OVERVIEW_SKELETON_WIDTH = 400;
const _EMPTY_STATE_ICON_SIZE = 64;
// Spacing Constants
const HORIZONTAL_PADDING = 40;
const DOT_PADDING = 2;
const DOT_GAP = 4;
-const CONTROLS_GAP = 20;
+const CONTROLS_GAP = 10;
const _TEXT_MARGIN_TOP = 16;
// Border Radius Constants
@@ -88,13 +92,16 @@ const VELOCITY_THRESHOLD = 400;
// Text Constants
const GENRES_FONT_SIZE = 16;
+const OVERVIEW_FONT_SIZE = 14;
const _EMPTY_STATE_FONT_SIZE = 18;
const TEXT_SHADOW_RADIUS = 2;
const MAX_GENRES_COUNT = 2;
const MAX_BUTTON_WIDTH = 300;
+const OVERVIEW_MAX_LINES = 2;
+const OVERVIEW_MAX_WIDTH = "80%";
// Opacity Constants
-const OVERLAY_OPACITY = 0.4;
+const OVERLAY_OPACITY = 0.3;
const DOT_INACTIVE_OPACITY = 0.6;
const TEXT_OPACITY = 0.9;
@@ -180,7 +187,7 @@ export const AppleTVCarousel: React.FC = ({
userId: user.Id,
enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"],
includeItemTypes: ["Movie", "Series", "Episode"],
- fields: ["Genres"],
+ fields: ["Genres", "Overview"],
limit: 2,
});
return response.data.Items || [];
@@ -195,7 +202,7 @@ export const AppleTVCarousel: React.FC = ({
if (!api || !user?.Id) return [];
const response = await getTvShowsApi(api).getNextUp({
userId: user.Id,
- fields: ["MediaSourceCount", "Genres"],
+ fields: ["MediaSourceCount", "Genres", "Overview"],
limit: 2,
enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"],
enableResumable: false,
@@ -214,7 +221,7 @@ export const AppleTVCarousel: React.FC = ({
const response = await getUserLibraryApi(api).getLatestMedia({
userId: user.Id,
limit: 2,
- fields: ["PrimaryImageAspectRatio", "Path", "Genres"],
+ fields: ["PrimaryImageAspectRatio", "Path", "Genres", "Overview"],
imageTypeLimit: 1,
enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"],
});
@@ -374,6 +381,8 @@ export const AppleTVCarousel: React.FC = ({
};
});
+ const togglePlayedStatus = useMarkAsPlayed(items);
+
const headerAnimatedStyle = useAnimatedStyle(() => {
if (!scrollOffset) return {};
return {
@@ -521,6 +530,36 @@ export const AppleTVCarousel: React.FC = ({
/>
+ {/* Overview Skeleton */}
+
+
+
+
+
{/* Controls Skeleton */}
= ({
+ {/* Overview Section - for Episodes and Movies */}
+ {(item.Type === "Episode" || item.Type === "Movie") &&
+ item.Overview && (
+
+ navigateToItem(item)}>
+
+ {item.Overview}
+
+
+
+ )}
+
{/* Controls Section */}
= ({
{/* Mark as Played */}
-
+
diff --git a/components/apple-tv-carousel/MarkAsPlayedLargeButton.tsx b/components/apple-tv-carousel/MarkAsPlayedLargeButton.tsx
new file mode 100644
index 00000000..ea9bd98d
--- /dev/null
+++ b/components/apple-tv-carousel/MarkAsPlayedLargeButton.tsx
@@ -0,0 +1,51 @@
+import { Button, Host } from "@expo/ui/swift-ui";
+import { Ionicons } from "@expo/vector-icons";
+import { Platform, View } from "react-native";
+import { RoundButton } from "../RoundButton";
+
+interface MarkAsPlayedLargeButtonProps {
+ isPlayed: boolean;
+ onToggle: (isPlayed: boolean) => void;
+}
+
+export const MarkAsPlayedLargeButton: React.FC<
+ MarkAsPlayedLargeButtonProps
+> = ({ isPlayed, onToggle }) => {
+ if (Platform.OS === "ios")
+ return (
+
+ onToggle(isPlayed)} variant='glass'>
+
+
+
+
+
+ );
+
+ return (
+
+ onToggle(isPlayed)}
+ />
+
+ );
+};
diff --git a/components/common/Dropdown.tsx b/components/common/Dropdown.tsx
deleted file mode 100644
index 3ec4d5ce..00000000
--- a/components/common/Dropdown.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
-import {
- type PropsWithChildren,
- type ReactNode,
- useEffect,
- useState,
-} from "react";
-import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
-import { Text } from "@/components/common/Text";
-import DisabledSetting from "@/components/settings/DisabledSetting";
-
-interface Props {
- data: T[];
- disabled?: boolean;
- placeholderText?: string;
- keyExtractor: (item: T) => string;
- titleExtractor: (item: T) => string | undefined;
- title: string | ReactNode;
- label: string;
- onSelected: (...item: T[]) => void;
- multiple?: boolean;
-}
-
-const Dropdown = ({
- data,
- disabled,
- placeholderText,
- keyExtractor,
- titleExtractor,
- title,
- label,
- onSelected,
- multiple = false,
- ...props
-}: PropsWithChildren & ViewProps>) => {
- const isTv = Platform.isTV;
-
- const [selected, setSelected] = useState();
-
- useEffect(() => {
- if (selected !== undefined) {
- onSelected(...selected);
- }
- }, [selected, onSelected]);
-
- if (isTv) return null;
-
- return (
-
-
-
- {typeof title === "string" ? (
-
- {title}
-
-
- {selected?.length !== undefined
- ? selected.map(titleExtractor).join(",")
- : placeholderText}
-
-
-
- ) : (
- title
- )}
-
-
- {label}
- {data.map((item, _idx) =>
- multiple ? (
- keyExtractor(s) === keyExtractor(item))
- ? "on"
- : "off"
- }
- key={keyExtractor(item)}
- onValueChange={(
- next: "on" | "off",
- _previous: "on" | "off",
- ) => {
- setSelected((p) => {
- const prev = p || [];
- if (next === "on") {
- return [...prev, item];
- }
- return [
- ...prev.filter(
- (p) => keyExtractor(p) !== keyExtractor(item),
- ),
- ];
- });
- }}
- >
-
- {titleExtractor(item)}
-
-
- ) : (
- setSelected([item])}
- >
-
- {titleExtractor(item)}
-
-
- ),
- )}
-
-
-
- );
-};
-
-export default Dropdown;
diff --git a/components/common/HeaderBackButton.tsx b/components/common/HeaderBackButton.tsx
index 3f75c83a..686cab5d 100644
--- a/components/common/HeaderBackButton.tsx
+++ b/components/common/HeaderBackButton.tsx
@@ -55,7 +55,7 @@ export const HeaderBackButton: React.FC = ({
return (
router.back()}
- className=' bg-neutral-800/80 rounded-full p-2'
+ className=' rounded-full p-2'
{...touchableOpacityProps}
>
= Partial & Pick;
-
export interface HorizontalScrollRef {
scrollToIndex: (index: number, viewOffset: number) => void;
}
interface HorizontalScrollProps
- extends PartialExcept<
- Omit, "renderItem">,
- "estimatedItemSize"
- > {
+ extends Omit, "renderItem" | "estimatedItemSize" | "data"> {
data?: T[] | null;
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor?: (item: T, index: number) => string;
@@ -44,7 +39,7 @@ export const HorizontalScroll = (
...restProps
} = props;
- const flashListRef = useRef>(null);
+ const flashListRef = useRef>>(null);
useImperativeHandle(ref!, () => ({
scrollToIndex: (index: number, viewOffset: number) => {
@@ -78,7 +73,6 @@ export const HorizontalScroll = (
extraData={extraData}
renderItem={renderFlashListItem}
horizontal
- estimatedItemSize={200}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
paddingHorizontal: 16,
diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/JellyseerrItemRouter.tsx
index 8a0b059a..2fdaa801 100644
--- a/components/common/JellyseerrItemRouter.tsx
+++ b/components/common/JellyseerrItemRouter.tsx
@@ -1,14 +1,8 @@
import { useRouter, useSegments } from "expo-router";
import type React from "react";
-import { type PropsWithChildren, useCallback, useMemo } from "react";
+import { type PropsWithChildren } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
-import * as ContextMenu from "zeego/context-menu";
-import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
-import {
- hasPermission,
- Permission,
-} from "@/utils/jellyseerr/server/lib/permissions";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type {
@@ -38,90 +32,33 @@ export const TouchableJellyseerrRouter: React.FC> = ({
}) => {
const router = useRouter();
const segments = useSegments();
- const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
const from = (segments as string[])[2] || "(home)";
- const autoApprove = useMemo(() => {
- return (
- jellyseerrUser &&
- hasPermission(Permission.AUTO_APPROVE, jellyseerrUser.permissions, {
- type: "or",
- })
- );
- }, [jellyseerrApi, jellyseerrUser]);
-
- const request = useCallback(() => {
- if (!result) return;
- requestMedia(mediaTitle, {
- mediaId: result.id,
- mediaType,
- });
- }, [jellyseerrApi, result]);
-
if (from === "(home)" || from === "(search)" || from === "(libraries)")
return (
-
-
- {
- if (!result) return;
+ {
+ if (!result) return;
- router.push({
- pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
- // @ts-expect-error
- params: {
- ...result,
- mediaTitle,
- releaseYear,
- canRequest: canRequest.toString(),
- posterSrc,
- mediaType,
- },
- });
- }}
- {...props}
- >
- {children}
-
-
-
- Actions
- {canRequest && mediaType === MediaType.MOVIE && (
- {
- if (autoApprove) {
- request();
- }
- }}
- shouldDismissMenuOnSelect
- >
-
- Request
-
-
-
- )}
-
-
+ router.push({
+ pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
+ // @ts-expect-error
+ params: {
+ ...result,
+ mediaTitle,
+ releaseYear,
+ canRequest: canRequest.toString(),
+ posterSrc,
+ mediaType,
+ },
+ });
+ }}
+ {...props}
+ >
+ {children}
+
);
+
+ return null;
};
diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx
index 97b197c4..3dba2418 100644
--- a/components/downloads/ActiveDownloads.tsx
+++ b/components/downloads/ActiveDownloads.tsx
@@ -26,7 +26,7 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
{t("home.downloads.active_downloads")}
-
+
{processes?.map((p: JobStatus) => (
))}
diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx
index b90c4836..3f994a16 100644
--- a/components/downloads/DownloadCard.tsx
+++ b/components/downloads/DownloadCard.tsx
@@ -6,7 +6,6 @@ import { t } from "i18next";
import { useMemo } from "react";
import {
ActivityIndicator,
- Platform,
TouchableOpacity,
type TouchableOpacityProps,
View,
@@ -14,49 +13,36 @@ import {
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
+import { calculateSmoothedETA } from "@/providers/Downloads/hooks/useDownloadSpeedCalculator";
import { JobStatus } from "@/providers/Downloads/types";
+import { estimateDownloadSize } from "@/utils/download";
import { storage } from "@/utils/mmkv";
import { formatTimeString } from "@/utils/time";
-import { Button } from "../Button";
const bytesToMB = (bytes: number) => {
return bytes / 1024 / 1024;
};
+const formatBytes = (bytes: number): string => {
+ if (bytes >= 1024 * 1024 * 1024) {
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
+ }
+ return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
+};
+
interface DownloadCardProps extends TouchableOpacityProps {
process: JobStatus;
}
export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
- const { startDownload, pauseDownload, resumeDownload, removeProcess } =
- useDownload();
+ const { cancelDownload } = useDownload();
const router = useRouter();
const queryClient = useQueryClient();
- const handlePause = async (id: string) => {
- try {
- await pauseDownload(id);
- toast.success(t("home.downloads.toasts.download_paused"));
- } catch (error) {
- console.error("Error pausing download:", error);
- toast.error(t("home.downloads.toasts.could_not_pause_download"));
- }
- };
-
- const handleResume = async (id: string) => {
- try {
- await resumeDownload(id);
- toast.success(t("home.downloads.toasts.download_resumed"));
- } catch (error) {
- console.error("Error resuming download:", error);
- toast.error(t("home.downloads.toasts.could_not_resume_download"));
- }
- };
-
const handleDelete = async (id: string) => {
try {
- await removeProcess(id);
- toast.success(t("home.downloads.toasts.download_deleted"));
+ await cancelDownload(id);
+ // cancelDownload already shows a toast, so don't show another one
queryClient.invalidateQueries({ queryKey: ["downloads"] });
} catch (error) {
console.error("Error deleting download:", error);
@@ -64,16 +50,48 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
}
};
- const eta = (p: JobStatus) => {
- if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
+ const eta = useMemo(() => {
+ if (!process.estimatedTotalSizeBytes || !process.bytesDownloaded) {
+ return null;
+ }
- const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
- if (bytesRemaining <= 0) return null;
+ const secondsRemaining = calculateSmoothedETA(
+ process.id,
+ process.bytesDownloaded,
+ process.estimatedTotalSizeBytes,
+ );
- const secondsRemaining = bytesRemaining / p.speed;
+ if (!secondsRemaining || secondsRemaining <= 0) {
+ return null;
+ }
return formatTimeString(secondsRemaining, "s");
- };
+ }, [process.id, process.bytesDownloaded, process.estimatedTotalSizeBytes]);
+
+ const estimatedSize = useMemo(() => {
+ if (process.estimatedTotalSizeBytes) return process.estimatedTotalSizeBytes;
+
+ // Calculate from bitrate + duration (only if bitrate value is defined)
+ if (process.maxBitrate.value) {
+ return estimateDownloadSize(
+ process.maxBitrate.value,
+ process.item.RunTimeTicks,
+ );
+ }
+
+ return undefined;
+ }, [
+ process.maxBitrate.value,
+ process.item.RunTimeTicks,
+ process.estimatedTotalSizeBytes,
+ ]);
+
+ const isTranscoding = process.isTranscoding || false;
+
+ const downloadedAmount = useMemo(() => {
+ if (!process.bytesDownloaded) return null;
+ return formatBytes(process.bytesDownloaded);
+ }, [process.bytesDownloaded]);
const base64Image = useMemo(() => {
return storage.getString(process.item.Id!);
@@ -98,9 +116,7 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
>
{process.status === "downloading" && (
0
@@ -111,26 +127,10 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
)}
{/* Action buttons in bottom right corner */}
-
- {process.status === "downloading" && Platform.OS !== "ios" && (
- handlePause(process.id)}
- className='p-1'
- >
-
-
- )}
- {process.status === "paused" && Platform.OS !== "ios" && (
- handleResume(process.id)}
- className='p-1'
- >
-
-
- )}
+
handleDelete(process.id)}
- className='p-1'
+ className='p-2 bg-neutral-800 rounded-full'
>
@@ -152,47 +152,53 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
/>
)}
-
+
{process.item.Type}
{process.item.Name}
{process.item.ProductionYear}
-
+
+ {isTranscoding && (
+
+ Transcoding
+
+ )}
+
+ {/* Row 1: Progress + Downloaded/Total */}
+
{sanitizedProgress === 0 ? (
) : (
- {sanitizedProgress.toFixed(0)}%
- )}
- {process.speed && process.speed > 0 && (
-
- {bytesToMB(process.speed).toFixed(2)} MB/s
+
+ {sanitizedProgress.toFixed(0)}%
)}
- {eta(process) && (
-
- {t("home.downloads.eta", { eta: eta(process) })}
+ {downloadedAmount && (
+
+ {downloadedAmount}
+ {estimatedSize
+ ? ` / ${isTranscoding ? "~" : ""}${formatBytes(estimatedSize)}`
+ : ""}
)}
-
- {process.status}
+ {/* Row 2: Speed + ETA */}
+
+ {process.speed && process.speed > 0 && (
+
+ {bytesToMB(process.speed).toFixed(2)} MB/s
+
+ )}
+ {eta && (
+
+ {t("home.downloads.eta", { eta: eta })}
+
+ )}
- {process.status === "completed" && (
-
- {
- startDownload(process);
- }}
- className='w-full'
- >
- Download now
-
-
- )}
);
diff --git a/components/downloads/DownloadSize.tsx b/components/downloads/DownloadSize.tsx
index c6316ff9..32993f37 100644
--- a/components/downloads/DownloadSize.tsx
+++ b/components/downloads/DownloadSize.tsx
@@ -13,14 +13,13 @@ export const DownloadSize: React.FC = ({
items,
...props
}) => {
- const { getDownloadedItemSize, getDownloadedItems } = useDownload();
- const downloadedFiles = getDownloadedItems();
+ const { getDownloadedItemSize, downloadedItems } = useDownload();
const [size, setSize] = useState();
const itemIds = useMemo(() => items.map((i) => i.Id), [items]);
useEffect(() => {
- if (!downloadedFiles) return;
+ if (!downloadedItems) return;
let s = 0;
@@ -32,7 +31,7 @@ export const DownloadSize: React.FC = ({
}
}
setSize(s.bytesToReadable());
- }, [itemIds]);
+ }, [itemIds, downloadedItems, getDownloadedItemSize]);
const sizeText = useMemo(() => {
if (!size) return "...";
diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx
index 83c0a4e7..3c57a17a 100644
--- a/components/downloads/EpisodeCard.tsx
+++ b/components/downloads/EpisodeCard.tsx
@@ -28,7 +28,7 @@ export const EpisodeCard: React.FC = ({ item }) => {
*/
const handleDeleteFile = useCallback(() => {
if (item.Id) {
- deleteFile(item.Id, "Episode");
+ deleteFile(item.Id);
successHapticFeedback();
}
}, [deleteFile, item.Id]);
diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx
index 8b2d4911..85b1a89e 100644
--- a/components/downloads/SeriesCard.tsx
+++ b/components/downloads/SeriesCard.tsx
@@ -19,7 +19,13 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
return storage.getString(items[0].SeriesId!);
}, []);
- const deleteSeries = useCallback(async () => deleteItems(items), [items]);
+ const deleteSeries = useCallback(
+ async () =>
+ deleteItems(
+ items.map((item) => item.Id).filter((id) => id !== undefined),
+ ),
+ [items],
+ );
const showActionSheet = useCallback(() => {
const options = ["Delete", "Cancel"];
diff --git a/components/home/Home.tsx b/components/home/Home.tsx
new file mode 100644
index 00000000..44298311
--- /dev/null
+++ b/components/home/Home.tsx
@@ -0,0 +1,515 @@
+import { Feather, Ionicons } from "@expo/vector-icons";
+import type { Api } from "@jellyfin/sdk";
+import type {
+ BaseItemDto,
+ BaseItemDtoQueryResult,
+ BaseItemKind,
+} from "@jellyfin/sdk/lib/generated-client/models";
+import {
+ getItemsApi,
+ getSuggestionsApi,
+ getTvShowsApi,
+ getUserLibraryApi,
+ getUserViewsApi,
+} from "@jellyfin/sdk/lib/utils/api";
+import { type QueryFunction, useQuery } from "@tanstack/react-query";
+import { useNavigation, useRouter, useSegments } from "expo-router";
+import { useAtomValue } from "jotai";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ ActivityIndicator,
+ Platform,
+ RefreshControl,
+ ScrollView,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { Button } from "@/components/Button";
+import { Text } from "@/components/common/Text";
+import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
+import { Loader } from "@/components/Loader";
+import { MediaListSection } from "@/components/medialists/MediaListSection";
+import { Colors } from "@/constants/Colors";
+import { useNetworkStatus } from "@/hooks/useNetworkStatus";
+import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
+import { useDownload } from "@/providers/DownloadProvider";
+import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
+import { useSettings } from "@/utils/atoms/settings";
+import { eventBus } from "@/utils/eventBus";
+
+type ScrollingCollectionListSection = {
+ type: "ScrollingCollectionList";
+ title?: string;
+ queryKey: (string | undefined | null)[];
+ queryFn: QueryFunction;
+ orientation?: "horizontal" | "vertical";
+};
+
+type MediaListSectionType = {
+ type: "MediaListSection";
+ queryKey: (string | undefined)[];
+ queryFn: QueryFunction;
+};
+
+type Section = ScrollingCollectionListSection | MediaListSectionType;
+
+export const Home = () => {
+ const router = useRouter();
+ const { t } = useTranslation();
+ const api = useAtomValue(apiAtom);
+ const user = useAtomValue(userAtom);
+ const insets = useSafeAreaInsets();
+ const [loading, setLoading] = useState(false);
+ const { settings, refreshStreamyfinPluginSettings } = useSettings();
+ const navigation = useNavigation();
+ const scrollRef = useRef(null);
+ const { downloadedItems, cleanCacheDirectory } = useDownload();
+ const prevIsConnected = useRef(false);
+ const {
+ isConnected,
+ serverConnected,
+ loading: retryLoading,
+ retryCheck,
+ } = useNetworkStatus();
+ const invalidateCache = useInvalidatePlaybackProgressCache();
+
+ useEffect(() => {
+ if (isConnected && !prevIsConnected.current) {
+ invalidateCache();
+ }
+ prevIsConnected.current = isConnected;
+ }, [isConnected, invalidateCache]);
+
+ const hasDownloads = useMemo(() => {
+ if (Platform.isTV) return false;
+ return downloadedItems.length > 0;
+ }, [downloadedItems]);
+
+ useEffect(() => {
+ if (Platform.isTV) {
+ navigation.setOptions({
+ headerLeft: () => null,
+ });
+ return;
+ }
+ navigation.setOptions({
+ headerLeft: () => (
+ {
+ router.push("/(auth)/downloads");
+ }}
+ className='ml-1.5'
+ style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
+ >
+
+
+ ),
+ });
+ }, [navigation, router, hasDownloads]);
+
+ useEffect(() => {
+ cleanCacheDirectory().catch((_e) =>
+ console.error("Something went wrong cleaning cache directory"),
+ );
+ }, []);
+
+ const segments = useSegments();
+ useEffect(() => {
+ const unsubscribe = eventBus.on("scrollToTop", () => {
+ if ((segments as string[])[2] === "(home)")
+ scrollRef.current?.scrollTo({
+ y: Platform.isTV ? -152 : -100,
+ animated: true,
+ });
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [segments]);
+
+ const {
+ data,
+ isError: e1,
+ isLoading: l1,
+ } = useQuery({
+ queryKey: ["home", "userViews", user?.Id],
+ queryFn: async () => {
+ if (!api || !user?.Id) {
+ return null;
+ }
+
+ const response = await getUserViewsApi(api).getUserViews({
+ userId: user.Id,
+ });
+
+ return response.data.Items || null;
+ },
+ enabled: !!api && !!user?.Id,
+ staleTime: 60 * 1000,
+ });
+
+ const userViews = useMemo(
+ () => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
+ [data, settings?.hiddenLibraries],
+ );
+
+ const collections = useMemo(() => {
+ const allow = ["movies", "tvshows"];
+ return (
+ userViews?.filter(
+ (c) => c.CollectionType && allow.includes(c.CollectionType),
+ ) || []
+ );
+ }, [userViews]);
+
+ const refetch = async () => {
+ setLoading(true);
+ await refreshStreamyfinPluginSettings();
+ await invalidateCache();
+ setLoading(false);
+ };
+
+ const createCollectionConfig = useCallback(
+ (
+ title: string,
+ queryKey: string[],
+ includeItemTypes: BaseItemKind[],
+ parentId: string | undefined,
+ ): ScrollingCollectionListSection => ({
+ title,
+ queryKey,
+ queryFn: async () => {
+ if (!api) return [];
+ return (
+ (
+ await getUserLibraryApi(api).getLatestMedia({
+ userId: user?.Id,
+ limit: 20,
+ fields: ["PrimaryImageAspectRatio", "Path", "Genres"],
+ imageTypeLimit: 1,
+ enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"],
+ includeItemTypes,
+ parentId,
+ })
+ ).data || []
+ );
+ },
+ type: "ScrollingCollectionList",
+ }),
+ [api, user?.Id],
+ );
+
+ const defaultSections = useMemo(() => {
+ if (!api || !user?.Id) return [];
+
+ const latestMediaViews = collections.map((c) => {
+ const includeItemTypes: BaseItemKind[] =
+ c.CollectionType === "tvshows" || c.CollectionType === "movies"
+ ? []
+ : ["Movie"];
+ const title = t("home.recently_added_in", { libraryName: c.Name });
+ const queryKey: string[] = [
+ "home",
+ `recentlyAddedIn${c.CollectionType}`,
+ user.Id!,
+ c.Id!,
+ ];
+ return createCollectionConfig(
+ title || "",
+ queryKey,
+ includeItemTypes,
+ c.Id,
+ );
+ });
+
+ const ss: Section[] = [
+ {
+ title: t("home.continue_watching"),
+ queryKey: ["home", "resumeItems"],
+ queryFn: async () =>
+ (
+ await getItemsApi(api).getResumeItems({
+ userId: user.Id,
+ enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"],
+ includeItemTypes: ["Movie", "Series", "Episode"],
+ fields: ["Genres"],
+ })
+ ).data.Items || [],
+ type: "ScrollingCollectionList",
+ orientation: "horizontal",
+ },
+ {
+ title: t("home.next_up"),
+ queryKey: ["home", "nextUp-all"],
+ queryFn: async () =>
+ (
+ await getTvShowsApi(api).getNextUp({
+ userId: user?.Id,
+ fields: ["MediaSourceCount", "Genres"],
+ limit: 20,
+ enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"],
+ enableResumable: false,
+ })
+ ).data.Items || [],
+ type: "ScrollingCollectionList",
+ orientation: "horizontal",
+ },
+ ...latestMediaViews,
+ {
+ title: t("home.suggested_movies"),
+ queryKey: ["home", "suggestedMovies", user?.Id],
+ queryFn: async () =>
+ (
+ await getSuggestionsApi(api).getSuggestions({
+ userId: user?.Id,
+ limit: 10,
+ mediaType: ["Video"],
+ type: ["Movie"],
+ })
+ ).data.Items || [],
+ type: "ScrollingCollectionList",
+ orientation: "vertical",
+ },
+ {
+ title: t("home.suggested_episodes"),
+ queryKey: ["home", "suggestedEpisodes", user?.Id],
+ queryFn: async () => {
+ try {
+ const suggestions = await getSuggestions(api, user.Id);
+ const nextUpPromises = suggestions.map((series) =>
+ getNextUp(api, user.Id, series.Id),
+ );
+ const nextUpResults = await Promise.all(nextUpPromises);
+
+ return nextUpResults.filter((item) => item !== null) || [];
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ return [];
+ }
+ },
+ type: "ScrollingCollectionList",
+ orientation: "horizontal",
+ },
+ ];
+ return ss;
+ }, [api, user?.Id, collections, t, createCollectionConfig]);
+
+ const customSections = useMemo(() => {
+ if (!api || !user?.Id || !settings?.home?.sections) return [];
+ const ss: Section[] = [];
+ settings.home.sections.forEach((section, index) => {
+ const id = section.title || `section-${index}`;
+ ss.push({
+ title: t(`${id}`),
+ queryKey: ["home", "custom", String(index), section.title ?? null],
+ queryFn: async () => {
+ if (section.items) {
+ const response = await getItemsApi(api).getItems({
+ userId: user?.Id,
+ limit: section.items?.limit || 25,
+ recursive: true,
+ includeItemTypes: section.items?.includeItemTypes,
+ sortBy: section.items?.sortBy,
+ sortOrder: section.items?.sortOrder,
+ filters: section.items?.filters,
+ parentId: section.items?.parentId,
+ });
+ return response.data.Items || [];
+ }
+ if (section.nextUp) {
+ const response = await getTvShowsApi(api).getNextUp({
+ userId: user?.Id,
+ fields: ["MediaSourceCount", "Genres"],
+ limit: section.nextUp?.limit || 25,
+ enableImageTypes: ["Primary", "Backdrop", "Thumb", "Logo"],
+ enableResumable: section.nextUp?.enableResumable,
+ enableRewatching: section.nextUp?.enableRewatching,
+ });
+ return response.data.Items || [];
+ }
+ if (section.latest) {
+ const response = await getUserLibraryApi(api).getLatestMedia({
+ userId: user?.Id,
+ includeItemTypes: section.latest?.includeItemTypes,
+ limit: section.latest?.limit || 25,
+ isPlayed: section.latest?.isPlayed,
+ groupItems: section.latest?.groupItems,
+ });
+ return response.data || [];
+ }
+ if (section.custom) {
+ const response = await api.get(
+ section.custom.endpoint,
+ {
+ params: { ...(section.custom.query || {}), userId: user?.Id },
+ headers: section.custom.headers || {},
+ },
+ );
+ return response.data.Items || [];
+ }
+ return [];
+ },
+ type: "ScrollingCollectionList",
+ orientation: section?.orientation || "vertical",
+ });
+ });
+ return ss;
+ }, [api, user?.Id, settings?.home?.sections]);
+
+ const sections = settings?.home?.sections ? customSections : defaultSections;
+
+ if (!isConnected || serverConnected !== true) {
+ let title = "";
+ let subtitle = "";
+
+ if (!isConnected) {
+ title = t("home.no_internet");
+ subtitle = t("home.no_internet_message");
+ } else if (serverConnected === null) {
+ title = t("home.checking_server_connection");
+ subtitle = t("home.checking_server_connection_message");
+ } else if (!serverConnected) {
+ title = t("home.server_unreachable");
+ subtitle = t("home.server_unreachable_message");
+ }
+ return (
+
+ {title}
+ {subtitle}
+
+
+ {!Platform.isTV && (
+ router.push("/(auth)/downloads")}
+ justify='center'
+ iconRight={
+
+ }
+ >
+ {t("home.go_to_downloads")}
+
+ )}
+
+
+ )
+ }
+ >
+ {retryLoading ? (
+
+ ) : (
+ t("home.retry")
+ )}
+
+
+
+ );
+ }
+
+ if (e1)
+ return (
+
+ {t("home.oops")}
+
+ {t("home.error_message")}
+
+
+ );
+
+ if (l1)
+ return (
+
+
+
+ );
+
+ return (
+
+ }
+ contentContainerStyle={{
+ paddingLeft: insets.left,
+ paddingRight: insets.right,
+ paddingBottom: 16,
+ }}
+ >
+
+ {sections.map((section, index) => {
+ if (section.type === "ScrollingCollectionList") {
+ return (
+
+ );
+ }
+ if (section.type === "MediaListSection") {
+ return (
+
+ );
+ }
+ return null;
+ })}
+
+
+ );
+};
+
+async function getSuggestions(api: Api, userId: string | undefined) {
+ if (!userId) return [];
+ const response = await getSuggestionsApi(api).getSuggestions({
+ userId,
+ limit: 10,
+ mediaType: ["Unknown"],
+ type: ["Series"],
+ });
+ return response.data.Items ?? [];
+}
+
+async function getNextUp(
+ api: Api,
+ userId: string | undefined,
+ seriesId: string | undefined,
+) {
+ if (!userId || !seriesId) return null;
+ const response = await getTvShowsApi(api).getNextUp({
+ userId,
+ seriesId,
+ limit: 1,
+ });
+ return response.data.Items?.[0] ?? null;
+}
diff --git a/components/settings/HomeIndex.tsx b/components/home/HomeWithCarousel.tsx
similarity index 86%
rename from components/settings/HomeIndex.tsx
rename to components/home/HomeWithCarousel.tsx
index 6fd24395..18b107fe 100644
--- a/components/settings/HomeIndex.tsx
+++ b/components/home/HomeWithCarousel.tsx
@@ -12,11 +12,7 @@ import {
getUserLibraryApi,
getUserViewsApi,
} from "@jellyfin/sdk/lib/utils/api";
-import {
- type QueryFunction,
- useQuery,
- useQueryClient,
-} from "@tanstack/react-query";
+import { type QueryFunction, useQuery } from "@tanstack/react-query";
import { useNavigation, useRouter, useSegments } from "expo-router";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -24,7 +20,6 @@ import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Platform,
- RefreshControl,
TouchableOpacity,
View,
} from "react-native";
@@ -45,7 +40,7 @@ import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
-import { AppleTVCarousel } from "../AppleTVCarousel";
+import { AppleTVCarousel } from "../apple-tv-carousel/AppleTVCarousel";
type ScrollingCollectionListSection = {
type: "ScrollingCollectionList";
@@ -63,32 +58,19 @@ type MediaListSectionType = {
type Section = ScrollingCollectionListSection | MediaListSectionType;
-export const HomeIndex = () => {
+export const HomeWithCarousel = () => {
const router = useRouter();
-
const { t } = useTranslation();
-
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
-
const insets = useSafeAreaInsets();
-
- const [loading, setLoading] = useState(false);
+ const [_loading, setLoading] = useState(false);
const { settings, refreshStreamyfinPluginSettings } = useSettings();
- const showLargeHomeCarousel = settings.showLargeHomeCarousel ?? true;
- const queryClient = useQueryClient();
- const headerOverlayOffset = Platform.isTV
- ? 0
- : showLargeHomeCarousel
- ? 60
- : 0;
-
+ const headerOverlayOffset = Platform.isTV ? 0 : 60;
const navigation = useNavigation();
-
const animatedScrollRef = useAnimatedRef();
const scrollOffset = useScrollViewOffset(animatedScrollRef);
-
- const { getDownloadedItems, cleanCacheDirectory } = useDownload();
+ const { downloadedItems, cleanCacheDirectory } = useDownload();
const prevIsConnected = useRef(false);
const {
isConnected,
@@ -97,15 +79,19 @@ export const HomeIndex = () => {
retryCheck,
} = useNetworkStatus();
const invalidateCache = useInvalidatePlaybackProgressCache();
+
useEffect(() => {
- // Only invalidate cache when transitioning from offline to online
if (isConnected && !prevIsConnected.current) {
invalidateCache();
}
- // Update the ref to the current state for the next render
prevIsConnected.current = isConnected;
}, [isConnected, invalidateCache]);
+ const hasDownloads = useMemo(() => {
+ if (Platform.isTV) return false;
+ return downloadedItems.length > 0;
+ }, [downloadedItems]);
+
useEffect(() => {
if (Platform.isTV) {
navigation.setOptions({
@@ -113,7 +99,6 @@ export const HomeIndex = () => {
});
return;
}
- const hasDownloads = getDownloadedItems().length > 0;
navigation.setOptions({
headerLeft: () => (
{
router.push("/(auth)/downloads");
}}
className='ml-1.5'
+ style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
>
{
),
});
- }, [navigation, router]);
+ }, [navigation, router, hasDownloads]);
useEffect(() => {
cleanCacheDirectory().catch((_e) =>
@@ -188,24 +174,13 @@ export const HomeIndex = () => {
);
}, [userViews]);
- const refetch = async () => {
+ const _refetch = async () => {
setLoading(true);
await refreshStreamyfinPluginSettings();
- await queryClient.clear();
await invalidateCache();
setLoading(false);
};
- useEffect(() => {
- const unsubscribe = eventBus.on("refreshHome", () => {
- refetch();
- });
-
- return () => {
- unsubscribe();
- };
- }, [refetch]);
-
const createCollectionConfig = useCallback(
(
title: string,
@@ -236,7 +211,6 @@ export const HomeIndex = () => {
[api, user?.Id],
);
- // Always call useMemo() at the top-level, using computed dependencies for both "default"/custom sections
const defaultSections = useMemo(() => {
if (!api || !user?.Id) return [];
@@ -246,10 +220,10 @@ export const HomeIndex = () => {
? []
: ["Movie"];
const title = t("home.recently_added_in", { libraryName: c.Name });
- const queryKey = [
+ const queryKey: string[] = [
"home",
`recentlyAddedIn${c.CollectionType}`,
- user?.Id!,
+ user.Id!,
c.Id!,
];
return createCollectionConfig(
@@ -293,16 +267,6 @@ export const HomeIndex = () => {
orientation: "horizontal",
},
...latestMediaViews,
- // ...(mediaListCollections?.map(
- // (ml) =>
- // ({
- // title: ml.Name,
- // queryKey: ["home", "mediaList", ml.Id!],
- // queryFn: async () => ml,
- // type: "MediaListSection",
- // orientation: "vertical",
- // } as Section)
- // ) || []),
{
title: t("home.suggested_movies"),
queryKey: ["home", "suggestedMovies", user?.Id],
@@ -411,15 +375,12 @@ export const HomeIndex = () => {
let subtitle = "";
if (!isConnected) {
- // No network connection
title = t("home.no_internet");
subtitle = t("home.no_internet_message");
} else if (serverConnected === null) {
- // Network is up, but server is being checked
title = t("home.checking_server_connection");
subtitle = t("home.checking_server_connection_message");
} else if (!serverConnected) {
- // Network is up, but server is unreachable
title = t("home.server_unreachable");
subtitle = t("home.server_unreachable_message");
}
@@ -488,35 +449,18 @@ export const HomeIndex = () => {
nestedScrollEnabled
contentInsetAdjustmentBehavior='never'
scrollEventThrottle={16}
- bounces={!showLargeHomeCarousel}
- overScrollMode={showLargeHomeCarousel ? "never" : "auto"}
- refreshControl={
- showLargeHomeCarousel ? undefined : (
-
- )
- }
+ bounces={false}
+ overScrollMode='never'
style={{ marginTop: -headerOverlayOffset }}
contentContainerStyle={{ paddingTop: headerOverlayOffset }}
>
- {showLargeHomeCarousel && (
-
- )}
+
@@ -551,7 +495,6 @@ export const HomeIndex = () => {
);
};
-// Function to get suggestions
async function getSuggestions(api: Api, userId: string | undefined) {
if (!userId) return [];
const response = await getSuggestionsApi(api).getSuggestions({
@@ -563,7 +506,6 @@ async function getSuggestions(api: Api, userId: string | undefined) {
return response.data.Items ?? [];
}
-// Function to get the next up TV show for a series
async function getNextUp(
api: Api,
userId: string | undefined,
diff --git a/components/jellyseerr/Cast.tsx b/components/jellyseerr/Cast.tsx
index 6abd890a..99440bd8 100644
--- a/components/jellyseerr/Cast.tsx
+++ b/components/jellyseerr/Cast.tsx
@@ -23,7 +23,6 @@ const CastSlide: React.FC<
showsHorizontalScrollIndicator={false}
data={details?.credits.cast}
ItemSeparatorComponent={() => }
- estimatedItemSize={15}
keyExtractor={(item) => item?.id?.toString()}
contentContainerStyle={{ paddingHorizontal: 16 }}
renderItem={({ item }) => (
diff --git a/components/jellyseerr/RequestModal.tsx b/components/jellyseerr/RequestModal.tsx
index 0dc521a7..36fff7ee 100644
--- a/components/jellyseerr/RequestModal.tsx
+++ b/components/jellyseerr/RequestModal.tsx
@@ -10,8 +10,8 @@ import { forwardRef, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import { Button } from "@/components/Button";
-import Dropdown from "@/components/common/Dropdown";
import { Text } from "@/components/common/Text";
+import { PlatformDropdown } from "@/components/PlatformDropdown";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import type {
QualityProfile,
@@ -48,8 +48,22 @@ const RequestModal = forwardRef<
userId: jellyseerrUser?.id,
});
+ const [qualityProfileOpen, setQualityProfileOpen] = useState(false);
+ const [rootFolderOpen, setRootFolderOpen] = useState(false);
+ const [tagsOpen, setTagsOpen] = useState(false);
+ const [usersOpen, setUsersOpen] = useState(false);
+
const { t } = useTranslation();
+ // Reset all dropdown states when modal closes
+ const handleDismiss = useCallback(() => {
+ setQualityProfileOpen(false);
+ setRootFolderOpen(false);
+ setTagsOpen(false);
+ setUsersOpen(false);
+ onDismiss?.();
+ }, [onDismiss]);
+
const { data: serviceSettings } = useQuery({
queryKey: ["jellyseerr", "request", type, "service"],
queryFn: async () =>
@@ -138,6 +152,109 @@ const RequestModal = forwardRef<
});
}, [requestBody?.seasons]);
+ const pathTitleExtractor = (item: RootFolder) =>
+ `${item.path} (${item.freeSpace.bytesToReadable()})`;
+
+ const qualityProfileOptions = useMemo(
+ () => [
+ {
+ options:
+ defaultServiceDetails?.profiles.map((profile) => ({
+ type: "radio" as const,
+ label: profile.name,
+ value: profile.id.toString(),
+ selected:
+ (requestOverrides.profileId || defaultProfile?.id) ===
+ profile.id,
+ onPress: () =>
+ setRequestOverrides((prev) => ({
+ ...prev,
+ profileId: profile.id,
+ })),
+ })) || [],
+ },
+ ],
+ [
+ defaultServiceDetails?.profiles,
+ defaultProfile,
+ requestOverrides.profileId,
+ ],
+ );
+
+ const rootFolderOptions = useMemo(
+ () => [
+ {
+ options:
+ defaultServiceDetails?.rootFolders.map((folder) => ({
+ type: "radio" as const,
+ label: pathTitleExtractor(folder),
+ value: folder.id.toString(),
+ selected:
+ (requestOverrides.rootFolder || defaultFolder?.path) ===
+ folder.path,
+ onPress: () =>
+ setRequestOverrides((prev) => ({
+ ...prev,
+ rootFolder: folder.path,
+ })),
+ })) || [],
+ },
+ ],
+ [
+ defaultServiceDetails?.rootFolders,
+ defaultFolder,
+ requestOverrides.rootFolder,
+ ],
+ );
+
+ const tagsOptions = useMemo(
+ () => [
+ {
+ options:
+ defaultServiceDetails?.tags.map((tag) => ({
+ type: "toggle" as const,
+ label: tag.label,
+ value:
+ requestOverrides.tags?.includes(tag.id) ||
+ defaultTags.some((dt) => dt.id === tag.id),
+ onToggle: () =>
+ setRequestOverrides((prev) => {
+ const currentTags = prev.tags || defaultTags.map((t) => t.id);
+ const hasTag = currentTags.includes(tag.id);
+ return {
+ ...prev,
+ tags: hasTag
+ ? currentTags.filter((id) => id !== tag.id)
+ : [...currentTags, tag.id],
+ };
+ }),
+ })) || [],
+ },
+ ],
+ [defaultServiceDetails?.tags, defaultTags, requestOverrides.tags],
+ );
+
+ const usersOptions = useMemo(
+ () => [
+ {
+ options:
+ users?.map((user) => ({
+ type: "radio" as const,
+ label: user.displayName,
+ value: user.id.toString(),
+ selected:
+ (requestOverrides.userId || jellyseerrUser?.id) === user.id,
+ onPress: () =>
+ setRequestOverrides((prev) => ({
+ ...prev,
+ userId: user.id,
+ })),
+ })) || [],
+ },
+ ],
+ [users, jellyseerrUser, requestOverrides.userId],
+ );
+
const request = useCallback(() => {
const body = {
is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k,
@@ -163,15 +280,12 @@ const RequestModal = forwardRef<
defaultTags,
]);
- const pathTitleExtractor = (item: RootFolder) =>
- `${item.path} (${item.freeSpace.bytesToReadable()})`;
-
return (
)}
+ stackBehavior='push'
>
@@ -199,70 +314,112 @@ const RequestModal = forwardRef<
{defaultService && defaultServiceDetails && users && (
<>
- item.name}
- placeholderText={
- requestOverrides.profileName || defaultProfile.name
- }
- keyExtractor={(item) => item.id.toString()}
- label={t("jellyseerr.quality_profile")}
- onSelected={(item) =>
- item &&
- setRequestOverrides((prev) => ({
- ...prev,
- profileId: item?.id,
- }))
- }
- title={t("jellyseerr.quality_profile")}
- />
- item.id.toString()}
- label={t("jellyseerr.root_folder")}
- onSelected={(item) =>
- item &&
- setRequestOverrides((prev) => ({
- ...prev,
- rootFolder: item.path,
- }))
- }
- title={t("jellyseerr.root_folder")}
- />
- item.label}
- placeholderText={defaultTags.map((t) => t.label).join(",")}
- keyExtractor={(item) => item.id.toString()}
- label={t("jellyseerr.tags")}
- onSelected={(...selected) =>
- setRequestOverrides((prev) => ({
- ...prev,
- tags: selected.map((i) => i.id),
- }))
- }
- title={t("jellyseerr.tags")}
- />
- item.displayName}
- placeholderText={jellyseerrUser!.displayName}
- keyExtractor={(item) => item.id.toString() || ""}
- label={t("jellyseerr.request_as")}
- onSelected={(item) =>
- item &&
- setRequestOverrides((prev) => ({
- ...prev,
- userId: item?.id,
- }))
- }
- title={t("jellyseerr.request_as")}
- />
+
+
+ {t("jellyseerr.quality_profile")}
+
+
+
+ {defaultServiceDetails.profiles.find(
+ (p) =>
+ p.id ===
+ (requestOverrides.profileId ||
+ defaultProfile?.id),
+ )?.name || defaultProfile?.name}
+
+
+ }
+ title={t("jellyseerr.quality_profile")}
+ open={qualityProfileOpen}
+ onOpenChange={setQualityProfileOpen}
+ />
+
+
+
+
+ {t("jellyseerr.root_folder")}
+
+
+
+ {defaultServiceDetails.rootFolders.find(
+ (f) =>
+ f.path ===
+ (requestOverrides.rootFolder ||
+ defaultFolder?.path),
+ )
+ ? pathTitleExtractor(
+ defaultServiceDetails.rootFolders.find(
+ (f) =>
+ f.path ===
+ (requestOverrides.rootFolder ||
+ defaultFolder?.path),
+ )!,
+ )
+ : pathTitleExtractor(defaultFolder!)}
+
+
+ }
+ title={t("jellyseerr.root_folder")}
+ open={rootFolderOpen}
+ onOpenChange={setRootFolderOpen}
+ />
+
+
+
+
+ {t("jellyseerr.tags")}
+
+
+
+ {requestOverrides.tags
+ ? defaultServiceDetails.tags
+ .filter((t) =>
+ requestOverrides.tags!.includes(t.id),
+ )
+ .map((t) => t.label)
+ .join(", ") ||
+ defaultTags.map((t) => t.label).join(", ")
+ : defaultTags.map((t) => t.label).join(", ")}
+
+
+ }
+ title={t("jellyseerr.tags")}
+ open={tagsOpen}
+ onOpenChange={setTagsOpen}
+ />
+
+
+
+
+ {t("jellyseerr.request_as")}
+
+
+
+ {users.find(
+ (u) =>
+ u.id ===
+ (requestOverrides.userId || jellyseerrUser?.id),
+ )?.displayName || jellyseerrUser!.displayName}
+
+
+ }
+ title={t("jellyseerr.request_as")}
+ open={usersOpen}
+ onOpenChange={setUsersOpen}
+ />
+
>
)}
diff --git a/components/search/DiscoverFilters.tsx b/components/search/DiscoverFilters.tsx
new file mode 100644
index 00000000..2e844c88
--- /dev/null
+++ b/components/search/DiscoverFilters.tsx
@@ -0,0 +1,115 @@
+import { Button, ContextMenu, Host, Picker } from "@expo/ui/swift-ui";
+import { Platform, View } from "react-native";
+import { FilterButton } from "@/components/filters/FilterButton";
+import { JellyseerrSearchSort } from "@/components/jellyseerr/JellyseerrIndexPage";
+
+interface DiscoverFiltersProps {
+ searchFilterId: string;
+ orderFilterId: string;
+ jellyseerrOrderBy: JellyseerrSearchSort;
+ setJellyseerrOrderBy: (value: JellyseerrSearchSort) => void;
+ jellyseerrSortOrder: "asc" | "desc";
+ setJellyseerrSortOrder: (value: "asc" | "desc") => void;
+ t: (key: string) => string;
+}
+
+const sortOptions = Object.keys(JellyseerrSearchSort).filter((v) =>
+ Number.isNaN(Number(v)),
+);
+
+const orderOptions = ["asc", "desc"] as const;
+
+export const DiscoverFilters: React.FC = ({
+ searchFilterId,
+ orderFilterId,
+ jellyseerrOrderBy,
+ setJellyseerrOrderBy,
+ jellyseerrSortOrder,
+ setJellyseerrSortOrder,
+ t,
+}) => {
+ if (Platform.OS === "ios") {
+ return (
+
+
+
+
+
+
+
+ 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,
+ );
+ }}
+ />
+ t(`library.filters.${item}`))}
+ variant='menu'
+ selectedIndex={orderOptions.indexOf(jellyseerrSortOrder)}
+ onOptionSelected={(event: any) => {
+ const index = event.nativeEvent.index;
+ setJellyseerrSortOrder(orderOptions[index]);
+ }}
+ />
+
+
+
+ );
+ }
+
+ // Android UI
+ return (
+
+
+ Object.keys(JellyseerrSearchSort).filter((v) =>
+ Number.isNaN(Number(v)),
+ )
+ }
+ set={(value) => setJellyseerrOrderBy(value[0])}
+ values={[jellyseerrOrderBy]}
+ title={t("library.filters.sort_by")}
+ renderItemLabel={(item) =>
+ t(`home.settings.plugins.jellyseerr.order_by.${item}`)
+ }
+ disableSearch={true}
+ />
+ ["asc", "desc"]}
+ set={(value) => setJellyseerrSortOrder(value[0])}
+ values={[jellyseerrSortOrder]}
+ title={t("library.filters.sort_order")}
+ renderItemLabel={(item) => t(`library.filters.${item}`)}
+ disableSearch={true}
+ />
+
+ );
+};
diff --git a/components/search/SearchTabButtons.tsx b/components/search/SearchTabButtons.tsx
new file mode 100644
index 00000000..b312b82e
--- /dev/null
+++ b/components/search/SearchTabButtons.tsx
@@ -0,0 +1,76 @@
+import { Button, Host } from "@expo/ui/swift-ui";
+import { Platform, TouchableOpacity, View } from "react-native";
+import { Tag } from "@/components/GenreTags";
+
+type SearchType = "Library" | "Discover";
+
+interface SearchTabButtonsProps {
+ searchType: SearchType;
+ setSearchType: (type: SearchType) => void;
+ t: (key: string) => string;
+}
+
+export const SearchTabButtons: React.FC = ({
+ searchType,
+ setSearchType,
+ t,
+}) => {
+ if (Platform.OS === "ios") {
+ return (
+ <>
+
+ setSearchType("Library")}
+ >
+ {t("search.library")}
+
+
+
+ setSearchType("Discover")}
+ >
+ {t("search.discover")}
+
+
+ >
+ );
+ }
+
+ // Android UI
+ return (
+
+ setSearchType("Library")}>
+
+
+ setSearchType("Discover")}>
+
+
+
+ );
+};
diff --git a/components/series/SeasonDropdown.tsx b/components/series/SeasonDropdown.tsx
index e9b4f0e7..5f6b64cd 100644
--- a/components/series/SeasonDropdown.tsx
+++ b/components/series/SeasonDropdown.tsx
@@ -1,11 +1,9 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
-import { useEffect, useMemo } from "react";
-import { Platform, TouchableOpacity, View } from "react-native";
-
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
import { t } from "i18next";
+import { useEffect, useMemo, useState } from "react";
+import { Platform, TouchableOpacity, View } from "react-native";
import { Text } from "../common/Text";
+import { PlatformDropdown } from "../PlatformDropdown";
type Props = {
item: BaseItemDto;
@@ -33,6 +31,7 @@ export const SeasonDropdown: React.FC = ({
onSelect,
}) => {
const isTv = Platform.isTV;
+ const [open, setOpen] = useState(false);
const keys = useMemo(
() =>
@@ -55,6 +54,31 @@ export const SeasonDropdown: React.FC = ({
[state, item, keys],
);
+ const sortByIndex = (a: BaseItemDto, b: BaseItemDto) =>
+ Number(a[keys.index]) - Number(b[keys.index]);
+
+ const optionGroups = useMemo(
+ () => [
+ {
+ options:
+ seasons?.sort(sortByIndex).map((season: any) => {
+ const title =
+ season[keys.title] ||
+ season.Name ||
+ `Season ${season.IndexNumber}`;
+ return {
+ type: "radio" as const,
+ label: title,
+ value: season.Id || season.IndexNumber,
+ selected: Number(season[keys.index]) === Number(seasonIndex),
+ onPress: () => onSelect(season),
+ };
+ }) || [],
+ },
+ ],
+ [seasons, keys, seasonIndex, onSelect],
+ );
+
useEffect(() => {
if (isTv) return;
if (seasons && seasons.length > 0 && seasonIndex === undefined) {
@@ -96,45 +120,23 @@ export const SeasonDropdown: React.FC = ({
keys,
]);
- const sortByIndex = (a: BaseItemDto, b: BaseItemDto) =>
- Number(a[keys.index]) - Number(b[keys.index]);
-
if (isTv) return null;
return (
-
-
-
-
+ setOpen(true)}>
+
{t("item_card.season")} {seasonIndex}
-
-
-
-
- {t("item_card.seasons")}
- {seasons?.sort(sortByIndex).map((season: any) => {
- const title =
- season[keys.title] || season.Name || `Season ${season.IndexNumber}`;
- return (
- onSelect(season)}
- >
- {title}
-
- );
- })}
-
-
+
+
+ }
+ title={t("item_card.seasons")}
+ />
);
};
diff --git a/components/series/SeasonEpisodesCarousel.tsx b/components/series/SeasonEpisodesCarousel.tsx
index e8242872..8e3051ee 100644
--- a/components/series/SeasonEpisodesCarousel.tsx
+++ b/components/series/SeasonEpisodesCarousel.tsx
@@ -29,7 +29,10 @@ export const SeasonEpisodesCarousel: React.FC = ({
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { getDownloadedItems } = useDownload();
- const downloadedFiles = getDownloadedItems();
+ const downloadedFiles = useMemo(
+ () => getDownloadedItems(),
+ [getDownloadedItems],
+ );
const scrollRef = useRef(null);
diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx
index cfb62972..f97f6c0e 100644
--- a/components/series/SeasonPicker.tsx
+++ b/components/series/SeasonPicker.tsx
@@ -86,7 +86,7 @@ export const SeasonPicker: React.FC = ({ item }) => {
seasonId: selectedSeasonId,
enableUserData: true,
// Note: Including trick play is necessary to enable trick play downloads
- fields: ["Overview", "Trickplay"],
+ fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
});
if (res.data.TotalRecordCount === 0)
diff --git a/components/settings/AppLanguageSelector.tsx b/components/settings/AppLanguageSelector.tsx
index f77bc2c8..ac52896c 100644
--- a/components/settings/AppLanguageSelector.tsx
+++ b/components/settings/AppLanguageSelector.tsx
@@ -1,12 +1,12 @@
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
+import { useMemo } from "react";
import { useTranslation } from "react-i18next";
-import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
+import { Platform, View, type ViewProps } from "react-native";
import { APP_LANGUAGES } from "@/i18n";
import { useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
+import { PlatformDropdown } from "../PlatformDropdown";
interface Props extends ViewProps {}
@@ -15,6 +15,31 @@ export const AppLanguageSelector: React.FC = () => {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation();
+ const optionGroups = useMemo(() => {
+ const options = [
+ {
+ type: "radio" as const,
+ label: t("home.settings.languages.system"),
+ value: "system",
+ selected: !settings?.preferedLanguage,
+ onPress: () => updateSettings({ preferedLanguage: undefined }),
+ },
+ ...APP_LANGUAGES.map((lang) => ({
+ type: "radio" as const,
+ label: lang.label,
+ value: lang.value,
+ selected: lang.value === settings?.preferedLanguage,
+ onPress: () => updateSettings({ preferedLanguage: lang.value }),
+ })),
+ ];
+
+ return [
+ {
+ options,
+ },
+ ];
+ }, [settings?.preferedLanguage, t, updateSettings]);
+
if (isTv) return null;
if (!settings) return null;
@@ -22,54 +47,19 @@ export const AppLanguageSelector: React.FC = () => {
-
-
-
+
{APP_LANGUAGES.find(
(l) => l.value === settings?.preferedLanguage,
)?.label || t("home.settings.languages.system")}
-
-
-
-
- {t("home.settings.languages.title")}
-
- {
- updateSettings({
- preferedLanguage: undefined,
- });
- }}
- >
-
- {t("home.settings.languages.system")}
-
-
- {APP_LANGUAGES?.map((l) => (
- {
- updateSettings({
- preferedLanguage: l.value,
- });
- }}
- >
- {l.label}
-
- ))}
-
-
+
+ }
+ title={t("home.settings.languages.title")}
+ />
diff --git a/components/settings/AudioToggles.tsx b/components/settings/AudioToggles.tsx
index 82ff1157..1a1c1457 100644
--- a/components/settings/AudioToggles.tsx
+++ b/components/settings/AudioToggles.tsx
@@ -1,14 +1,13 @@
-import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
-
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
import { Ionicons } from "@expo/vector-icons";
+import { useMemo } from "react";
import { useTranslation } from "react-i18next";
+import { Platform, View, type ViewProps } from "react-native";
import { Switch } from "react-native-gesture-handler";
import { useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
+import { PlatformDropdown } from "../PlatformDropdown";
import { useMedia } from "./MediaContext";
interface Props extends ViewProps {}
@@ -22,6 +21,39 @@ export const AudioToggles: React.FC = ({ ...props }) => {
const cultures = media.cultures;
const { t } = useTranslation();
+ const optionGroups = useMemo(() => {
+ const options = [
+ {
+ type: "radio" as const,
+ label: t("home.settings.audio.none"),
+ value: "none",
+ selected: !settings?.defaultAudioLanguage,
+ onPress: () => updateSettings({ defaultAudioLanguage: null }),
+ },
+ ...(cultures?.map((culture) => ({
+ type: "radio" as const,
+ label:
+ culture.DisplayName ||
+ culture.ThreeLetterISOLanguageName ||
+ "Unknown",
+ value:
+ culture.ThreeLetterISOLanguageName ||
+ culture.DisplayName ||
+ "unknown",
+ selected:
+ culture.ThreeLetterISOLanguageName ===
+ settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName,
+ onPress: () => updateSettings({ defaultAudioLanguage: culture }),
+ })) || []),
+ ];
+
+ return [
+ {
+ options,
+ },
+ ];
+ }, [cultures, settings?.defaultAudioLanguage, t, updateSettings]);
+
if (isTv) return null;
if (!settings) return null;
@@ -48,9 +80,10 @@ export const AudioToggles: React.FC = ({ ...props }) => {
/>
-
-
-
+
{settings?.defaultAudioLanguage?.DisplayName ||
t("home.settings.audio.none")}
@@ -60,48 +93,10 @@ export const AudioToggles: React.FC = ({ ...props }) => {
size={18}
color='#5A5960'
/>
-
-
-
-
- {t("home.settings.audio.language")}
-
- {
- updateSettings({
- defaultAudioLanguage: null,
- });
- }}
- >
-
- {t("home.settings.audio.none")}
-
-
- {cultures?.map((l) => (
- {
- updateSettings({
- defaultAudioLanguage: l,
- });
- }}
- >
-
- {l.DisplayName}
-
-
- ))}
-
-
+
+ }
+ title={t("home.settings.audio.language")}
+ />
diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx
index 6c12440c..3a0017ac 100644
--- a/components/settings/DownloadSettings.tsx
+++ b/components/settings/DownloadSettings.tsx
@@ -1,44 +1,3 @@
-import { useMemo } from "react";
-import { useTranslation } from "react-i18next";
-import { Stepper } from "@/components/inputs/Stepper";
-import DisabledSetting from "@/components/settings/DisabledSetting";
-import { type Settings, useSettings } from "@/utils/atoms/settings";
-import { ListGroup } from "../list/ListGroup";
-import { ListItem } from "../list/ListItem";
-
-export default function DownloadSettings({ ...props }) {
- const { settings, updateSettings, pluginSettings } = useSettings();
- const { t } = useTranslation();
-
- const allDisabled = useMemo(
- () =>
- pluginSettings?.remuxConcurrentLimit?.locked === true &&
- pluginSettings?.autoDownload?.locked === true,
- [pluginSettings],
- );
-
- if (!settings) return null;
-
- return (
-
-
-
-
- updateSettings({
- remuxConcurrentLimit: value as Settings["remuxConcurrentLimit"],
- })
- }
- />
-
-
-
- );
+export default function DownloadSettings() {
+ return null;
}
diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx
index e05e57b0..a93e219b 100644
--- a/components/settings/OtherSettings.tsx
+++ b/components/settings/OtherSettings.tsx
@@ -1,22 +1,15 @@
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
-import * as TaskManager from "expo-task-manager";
import { TFunction } from "i18next";
import type React from "react";
-import { useEffect, useMemo } from "react";
+import { useMemo } from "react";
import { useTranslation } from "react-i18next";
-import { Linking, Platform, Switch, TouchableOpacity } from "react-native";
-import { toast } from "sonner-native";
+import { Linking, Switch, View } from "react-native";
import { BITRATES } from "@/components/BitrateSelector";
-import Dropdown from "@/components/common/Dropdown";
+import { PlatformDropdown } from "@/components/PlatformDropdown";
import DisabledSetting from "@/components/settings/DisabledSetting";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
-import {
- BACKGROUND_FETCH_TASK,
- registerBackgroundFetchAsync,
- unregisterBackgroundFetchAsync,
-} from "@/utils/background-tasks";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
@@ -27,39 +20,8 @@ export const OtherSettings: React.FC = () => {
const { t } = useTranslation();
- /********************
- * Background task
- *******************/
- const checkStatusAsync = async () => {
- if (Platform.isTV) return false;
- return TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
- };
-
- useEffect(() => {
- (async () => {
- const registered = await checkStatusAsync();
-
- if (settings?.autoDownload === true && !registered) {
- registerBackgroundFetchAsync();
- toast.success(t("home.settings.toasts.background_downloads_enabled"));
- } else if (settings?.autoDownload === false && registered) {
- unregisterBackgroundFetchAsync();
- toast.info(t("home.settings.toasts.background_downloads_disabled"));
- } else if (settings?.autoDownload === true && registered) {
- // Don't to anything
- } else if (settings?.autoDownload === false && !registered) {
- // Don't to anything
- } else {
- updateSettings({ autoDownload: false });
- }
- })();
- }, [settings?.autoDownload]);
- /**********************
- *********************/
-
const disabled = useMemo(
() =>
- pluginSettings?.followDeviceOrientation?.locked === true &&
pluginSettings?.defaultVideoOrientation?.locked === true &&
pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
pluginSettings?.showCustomMenuLinks?.locked === true &&
@@ -89,41 +51,65 @@ export const OtherSettings: React.FC = () => {
[],
);
+ const orientationOptions = useMemo(
+ () => [
+ {
+ options: orientations.map((orientation) => ({
+ type: "radio" as const,
+ label: t(ScreenOrientationEnum[orientation]),
+ value: String(orientation),
+ selected: orientation === settings?.defaultVideoOrientation,
+ onPress: () =>
+ updateSettings({ defaultVideoOrientation: orientation }),
+ })),
+ },
+ ],
+ [orientations, settings?.defaultVideoOrientation, t, updateSettings],
+ );
+
+ const bitrateOptions = useMemo(
+ () => [
+ {
+ options: BITRATES.map((bitrate) => ({
+ type: "radio" as const,
+ label: bitrate.key,
+ value: bitrate.key,
+ selected: bitrate.key === settings?.defaultBitrate?.key,
+ onPress: () => updateSettings({ defaultBitrate: bitrate }),
+ })),
+ },
+ ],
+ [settings?.defaultBitrate?.key, t, updateSettings],
+ );
+
+ const autoPlayEpisodeOptions = useMemo(
+ () => [
+ {
+ options: AUTOPLAY_EPISODES_COUNT(t).map((item) => ({
+ type: "radio" as const,
+ label: item.key,
+ value: item.key,
+ selected: item.key === settings?.maxAutoPlayEpisodeCount?.key,
+ onPress: () => updateSettings({ maxAutoPlayEpisodeCount: item }),
+ })),
+ },
+ ],
+ [settings?.maxAutoPlayEpisodeCount?.key, t, updateSettings],
+ );
+
if (!settings) return null;
return (
-
-
- updateSettings({ followDeviceOrientation: value })
- }
- />
-
-
- t(ScreenOrientationEnum[item])}
- title={
-
+
{t(
orientationTranslations[
@@ -136,12 +122,9 @@ export const OtherSettings: React.FC = () => {
size={18}
color='#5A5960'
/>
-
- }
- label={t("home.settings.other.orientation")}
- onSelected={(defaultVideoOrientation) =>
- updateSettings({ defaultVideoOrientation })
+
}
+ title={t("home.settings.other.orientation")}
/>
@@ -222,13 +205,10 @@ export const OtherSettings: React.FC = () => {
title={t("home.settings.other.default_quality")}
disabled={pluginSettings?.defaultBitrate?.locked}
>
- item.key}
- titleExtractor={(item) => item.key}
- title={
-
+
{settings.defaultBitrate?.key}
@@ -237,10 +217,9 @@ export const OtherSettings: React.FC = () => {
size={18}
color='#5A5960'
/>
-
+
}
- label={t("home.settings.other.default_quality")}
- onSelected={(defaultBitrate) => updateSettings({ defaultBitrate })}
+ title={t("home.settings.other.default_quality")}
/>
{
/>
- item.key}
- titleExtractor={(item) => item.key}
- title={
-
+
{t(settings?.maxAutoPlayEpisodeCount.key)}
@@ -270,12 +247,9 @@ export const OtherSettings: React.FC = () => {
size={18}
color='#5A5960'
/>
-
- }
- label={t("home.settings.other.max_auto_play_episode_count")}
- onSelected={(maxAutoPlayEpisodeCount) =>
- updateSettings({ maxAutoPlayEpisodeCount })
+
}
+ title={t("home.settings.other.max_auto_play_episode_count")}
/>
diff --git a/components/settings/PluginSettings.tsx b/components/settings/PluginSettings.tsx
index 9d8c64dd..1fb064ef 100644
--- a/components/settings/PluginSettings.tsx
+++ b/components/settings/PluginSettings.tsx
@@ -14,7 +14,7 @@ export const PluginSettings = () => {
if (!settings) return null;
return (
-
+
= ({ ...props }) => {
const isTv = Platform.isTV;
@@ -27,18 +29,6 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
const cultures = media.cultures;
const { t } = useTranslation();
- // Get VLC subtitle settings from the settings system
- const textColor = settings?.vlcTextColor ?? "White";
- const backgroundColor = settings?.vlcBackgroundColor ?? "Black";
- const outlineColor = settings?.vlcOutlineColor ?? "Black";
- const outlineThickness = settings?.vlcOutlineThickness ?? "Normal";
- const backgroundOpacity = settings?.vlcBackgroundOpacity ?? 128;
- const outlineOpacity = settings?.vlcOutlineOpacity ?? 255;
- const isBold = settings?.vlcIsBold ?? false;
-
- if (isTv) return null;
- if (!settings) return null;
-
const subtitleModes = [
SubtitlePlaybackMode.Default,
SubtitlePlaybackMode.Smart,
@@ -56,6 +46,133 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
[SubtitlePlaybackMode.None]: "home.settings.subtitles.modes.None",
};
+ const subtitleLanguageOptionGroups = useMemo(() => {
+ const options = [
+ {
+ type: "radio" as const,
+ label: t("home.settings.subtitles.none"),
+ value: "none",
+ selected: !settings?.defaultSubtitleLanguage,
+ onPress: () => updateSettings({ defaultSubtitleLanguage: null }),
+ },
+ ...(cultures?.map((culture) => ({
+ type: "radio" as const,
+ label: culture.DisplayName || "Unknown",
+ value:
+ culture.ThreeLetterISOLanguageName ||
+ culture.DisplayName ||
+ "unknown",
+ selected:
+ culture.ThreeLetterISOLanguageName ===
+ settings?.defaultSubtitleLanguage?.ThreeLetterISOLanguageName,
+ onPress: () => updateSettings({ defaultSubtitleLanguage: culture }),
+ })) || []),
+ ];
+
+ return [
+ {
+ options,
+ },
+ ];
+ }, [cultures, settings?.defaultSubtitleLanguage, t, updateSettings]);
+
+ const subtitleModeOptionGroups = useMemo(() => {
+ const options = subtitleModes.map((mode) => ({
+ type: "radio" as const,
+ label: t(subtitleModeKeys[mode]) || String(mode),
+ value: String(mode),
+ selected: mode === settings?.subtitleMode,
+ onPress: () => updateSettings({ subtitleMode: mode }),
+ }));
+
+ return [
+ {
+ options,
+ },
+ ];
+ }, [settings?.subtitleMode, t, updateSettings]);
+
+ const textColorOptionGroups = useMemo(() => {
+ const colors = Object.keys(VLC_COLORS) as VLCColor[];
+ const options = colors.map((color) => ({
+ type: "radio" as const,
+ label: t(`home.settings.subtitles.colors.${color}`),
+ value: color,
+ selected: (settings?.vlcTextColor || "White") === color,
+ onPress: () => updateSettings({ vlcTextColor: color }),
+ }));
+
+ return [{ options }];
+ }, [settings?.vlcTextColor, t, updateSettings]);
+
+ const backgroundColorOptionGroups = useMemo(() => {
+ const colors = Object.keys(VLC_COLORS) as VLCColor[];
+ const options = colors.map((color) => ({
+ type: "radio" as const,
+ label: t(`home.settings.subtitles.colors.${color}`),
+ value: color,
+ selected: (settings?.vlcBackgroundColor || "Black") === color,
+ onPress: () => updateSettings({ vlcBackgroundColor: color }),
+ }));
+
+ return [{ options }];
+ }, [settings?.vlcBackgroundColor, t, updateSettings]);
+
+ const outlineColorOptionGroups = useMemo(() => {
+ const colors = Object.keys(VLC_COLORS) as VLCColor[];
+ const options = colors.map((color) => ({
+ type: "radio" as const,
+ label: t(`home.settings.subtitles.colors.${color}`),
+ value: color,
+ selected: (settings?.vlcOutlineColor || "Black") === color,
+ onPress: () => updateSettings({ vlcOutlineColor: color }),
+ }));
+
+ return [{ options }];
+ }, [settings?.vlcOutlineColor, t, updateSettings]);
+
+ const outlineThicknessOptionGroups = useMemo(() => {
+ const thicknesses = Object.keys(OUTLINE_THICKNESS) as OutlineThickness[];
+ const options = thicknesses.map((thickness) => ({
+ type: "radio" as const,
+ label: t(`home.settings.subtitles.thickness.${thickness}`),
+ value: thickness,
+ selected: (settings?.vlcOutlineThickness || "Normal") === thickness,
+ onPress: () => updateSettings({ vlcOutlineThickness: thickness }),
+ }));
+
+ return [{ options }];
+ }, [settings?.vlcOutlineThickness, t, updateSettings]);
+
+ const backgroundOpacityOptionGroups = useMemo(() => {
+ const opacities = [0, 32, 64, 96, 128, 160, 192, 224, 255];
+ const options = opacities.map((opacity) => ({
+ type: "radio" as const,
+ label: `${Math.round((opacity / 255) * 100)}%`,
+ value: opacity,
+ selected: (settings?.vlcBackgroundOpacity ?? 128) === opacity,
+ onPress: () => updateSettings({ vlcBackgroundOpacity: opacity }),
+ }));
+
+ return [{ options }];
+ }, [settings?.vlcBackgroundOpacity, updateSettings]);
+
+ const outlineOpacityOptionGroups = useMemo(() => {
+ const opacities = [0, 32, 64, 96, 128, 160, 192, 224, 255];
+ const options = opacities.map((opacity) => ({
+ type: "radio" as const,
+ label: `${Math.round((opacity / 255) * 100)}%`,
+ value: opacity,
+ selected: (settings?.vlcOutlineOpacity ?? 255) === opacity,
+ onPress: () => updateSettings({ vlcOutlineOpacity: opacity }),
+ }));
+
+ return [{ options }];
+ }, [settings?.vlcOutlineOpacity, updateSettings]);
+
+ if (isTv) return null;
+ if (!settings) return null;
+
return (
= ({ ...props }) => {
}
>
-
- item?.ThreeLetterISOLanguageName ?? "unknown"
- }
- titleExtractor={(item) => item?.DisplayName}
- title={
-
+
{settings?.defaultSubtitleLanguage?.DisplayName ||
t("home.settings.subtitles.none")}
@@ -90,18 +197,9 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
size={18}
color='#5A5960'
/>
-
- }
- label={t("home.settings.subtitles.language")}
- onSelected={(defaultSubtitleLanguage) =>
- updateSettings({
- defaultSubtitleLanguage:
- defaultSubtitleLanguage.DisplayName ===
- t("home.settings.subtitles.none")
- ? null
- : defaultSubtitleLanguage,
- })
+
}
+ title={t("home.settings.subtitles.language")}
/>
@@ -109,13 +207,10 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
title={t("home.settings.subtitles.subtitle_mode")}
disabled={pluginSettings?.subtitleMode?.locked}
>
- t(subtitleModeKeys[item]) || String(item)}
- title={
-
+
{t(subtitleModeKeys[settings?.subtitleMode]) ||
t("home.settings.subtitles.loading")}
@@ -125,10 +220,9 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
size={18}
color='#5A5960'
/>
-
+
}
- label={t("home.settings.subtitles.subtitle_mode")}
- onSelected={(subtitleMode) => updateSettings({ subtitleMode })}
+ title={t("home.settings.subtitles.subtitle_mode")}
/>
@@ -159,144 +253,120 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
/>
- item}
- titleExtractor={(item) =>
- t(`home.settings.subtitles.colors.${item}`)
- }
- title={
-
+
- {t(`home.settings.subtitles.colors.${textColor}`)}
+ {t(
+ `home.settings.subtitles.colors.${settings?.vlcTextColor || "White"}`,
+ )}
-
+
}
- label={t("home.settings.subtitles.text_color")}
- onSelected={(value) => updateSettings({ vlcTextColor: value })}
+ title={t("home.settings.subtitles.text_color")}
/>
- item}
- titleExtractor={(item) =>
- t(`home.settings.subtitles.colors.${item}`)
- }
- title={
-
+
- {t(`home.settings.subtitles.colors.${backgroundColor}`)}
+ {t(
+ `home.settings.subtitles.colors.${settings?.vlcBackgroundColor || "Black"}`,
+ )}
-
- }
- label={t("home.settings.subtitles.background_color")}
- onSelected={(value) =>
- updateSettings({ vlcBackgroundColor: value })
+
}
+ title={t("home.settings.subtitles.background_color")}
/>
- item}
- titleExtractor={(item) =>
- t(`home.settings.subtitles.colors.${item}`)
- }
- title={
-
+
- {t(`home.settings.subtitles.colors.${outlineColor}`)}
+ {t(
+ `home.settings.subtitles.colors.${settings?.vlcOutlineColor || "Black"}`,
+ )}
-
+
}
- label={t("home.settings.subtitles.outline_color")}
- onSelected={(value) => updateSettings({ vlcOutlineColor: value })}
+ title={t("home.settings.subtitles.outline_color")}
/>
- item}
- titleExtractor={(item) =>
- t(`home.settings.subtitles.thickness.${item}`)
- }
- title={
-
+
- {t(`home.settings.subtitles.thickness.${outlineThickness}`)}
+ {t(
+ `home.settings.subtitles.thickness.${settings?.vlcOutlineThickness || "Normal"}`,
+ )}
-
- }
- label={t("home.settings.subtitles.outline_thickness")}
- onSelected={(value) =>
- updateSettings({ vlcOutlineThickness: value })
+
}
+ title={t("home.settings.subtitles.outline_thickness")}
/>
- `${Math.round((item / 255) * 100)}%`}
- title={
-
- {`${Math.round((backgroundOpacity / 255) * 100)}%`}
+
+ {`${Math.round(((settings?.vlcBackgroundOpacity ?? 128) / 255) * 100)}%`}
-
- }
- label={t("home.settings.subtitles.background_opacity")}
- onSelected={(value) =>
- updateSettings({ vlcBackgroundOpacity: value })
+
}
+ title={t("home.settings.subtitles.background_opacity")}
/>
- `${Math.round((item / 255) * 100)}%`}
- title={
-
- {`${Math.round((outlineOpacity / 255) * 100)}%`}
+
+ {`${Math.round(((settings?.vlcOutlineOpacity ?? 255) / 255) * 100)}%`}
-
+
}
- label={t("home.settings.subtitles.outline_opacity")}
- onSelected={(value) => updateSettings({ vlcOutlineOpacity: value })}
+ title={t("home.settings.subtitles.outline_opacity")}
/>
updateSettings({ vlcIsBold: value })}
/>
diff --git a/components/video-player/controls/BottomControls.tsx b/components/video-player/controls/BottomControls.tsx
index 59da6f7b..a2652d70 100644
--- a/components/video-player/controls/BottomControls.tsx
+++ b/components/video-player/controls/BottomControls.tsx
@@ -114,10 +114,7 @@ export const BottomControls: FC = ({
}}
>
{item?.Type === "Episode" && (
@@ -133,7 +130,7 @@ export const BottomControls: FC = ({
{item?.Album}
)}
-
+
= ({
}>();
const { showSkipButton, skipIntro } = useIntroSkipper(
- item?.Id!,
+ item.Id!,
currentTime,
seek,
play,
@@ -332,7 +332,7 @@ export const Controls: FC = ({
);
const { showSkipCreditButton, skipCredit } = useCreditSkipper(
- item?.Id!,
+ item.Id!,
currentTime,
seek,
play,
diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx
index f39b0d7e..0a90b429 100644
--- a/components/video-player/controls/EpisodeList.tsx
+++ b/components/video-player/controls/EpisodeList.tsx
@@ -56,7 +56,10 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => {
}, []);
const { getDownloadedItems } = useDownload();
- const downloadedFiles = getDownloadedItems();
+ const downloadedFiles = useMemo(
+ () => getDownloadedItems(),
+ [getDownloadedItems],
+ );
const seasonIndex = seasonIndexState[item.ParentId ?? ""];
@@ -68,13 +71,13 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => {
const seriesEpisodes = downloadedFiles?.filter(
(f: DownloadedItem) => f.item.SeriesId === item.SeriesId,
);
- const seasonNumbers = [
- ...new Set(
+ const seasonNumbers = Array.from(
+ new Set(
seriesEpisodes
?.map((f: DownloadedItem) => f.item.ParentIndexNumber)
.filter(Boolean),
),
- ];
+ );
// Create fake season objects
return seasonNumbers.map((seasonNumber) => ({
Id: seasonNumber?.toString(),
diff --git a/components/video-player/controls/HeaderControls.tsx b/components/video-player/controls/HeaderControls.tsx
index 07232014..d8e4dcfe 100644
--- a/components/video-player/controls/HeaderControls.tsx
+++ b/components/video-player/controls/HeaderControls.tsx
@@ -111,7 +111,7 @@ export const HeaderControls: FC = ({
pointerEvents={showControls ? "auto" : "none"}
className={"flex flex-row w-full pt-2"}
>
-
+
{!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && (
= ({
setSubtitleTrack={setSubtitleTrack}
setSubtitleURL={setSubtitleURL}
>
-
+
+
+
)}
diff --git a/components/video-player/controls/ScaleFactorSelector.tsx b/components/video-player/controls/ScaleFactorSelector.tsx
index b6ed1853..0e5f2b10 100644
--- a/components/video-player/controls/ScaleFactorSelector.tsx
+++ b/components/video-player/controls/ScaleFactorSelector.tsx
@@ -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 = ({
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(() => {
+ 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 (
- <>
- (
+ setOpen(true)}
>
-
+
+ ),
+ [disabled],
+ );
- {
- 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);
- }
- }}
- />
- >
+ // Hide on TV platforms
+ if (Platform.isTV) return null;
+
+ return (
+
);
};
diff --git a/components/video-player/controls/SkipButton.tsx b/components/video-player/controls/SkipButton.tsx
index 016f94d1..2e34e7b7 100644
--- a/components/video-player/controls/SkipButton.tsx
+++ b/components/video-player/controls/SkipButton.tsx
@@ -13,6 +13,12 @@ const SkipButton: React.FC = ({
buttonText,
...props
}) => {
+ console.log(`[SKIP_BUTTON] Render:`, {
+ buttonText,
+ showButton,
+ className: showButton ? "flex" : "hidden",
+ });
+
return (
= ({
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(() => {
+ 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 (
- <>
- (
+ setOpen(true)}
>
-
+
+ ),
+ [disabled],
+ );
- {
- 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);
- }
- }}
- />
- >
+ // Hide on TV platforms
+ if (Platform.isTV) return null;
+
+ return (
+
);
};
diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx
index c6c8f82f..e1332e43 100644
--- a/components/video-player/controls/dropdown/DropdownView.tsx
+++ b/components/video-player/controls/dropdown/DropdownView.tsx
@@ -1,16 +1,12 @@
import { Ionicons } from "@expo/vector-icons";
-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 { useCallback, useMemo, useRef } from "react";
+import { Platform, View } from "react-native";
import { BITRATES } from "@/components/BitrateSelector";
-import { Text } from "@/components/common/Text";
+import {
+ type OptionGroup,
+ PlatformDropdown,
+} from "@/components/PlatformDropdown";
import { useControlContext } from "../contexts/ControlContext";
import { useVideoContext } from "../contexts/VideoContext";
@@ -23,10 +19,6 @@ 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<{
@@ -39,248 +31,127 @@ const DropdownView = () => {
offline: string;
}>();
+ // Use ref to track playbackPosition without causing re-renders
+ const playbackPositionRef = useRef(playbackPosition);
+ playbackPositionRef.current = playbackPosition;
+
const isOffline = offline === "true";
+ // Stabilize IDs to prevent unnecessary recalculations
+ const itemIdRef = useRef(item.Id);
+ const mediaSourceIdRef = useRef(mediaSource?.Id);
+ itemIdRef.current = item.Id;
+ mediaSourceIdRef.current = mediaSource?.Id;
+
const changeBitrate = useCallback(
(bitrate: string) => {
const queryParams = new URLSearchParams({
- itemId: item.Id ?? "",
+ itemId: itemIdRef.current ?? "",
audioIndex: audioIndex?.toString() ?? "",
- subtitleIndex: subtitleIndex.toString() ?? "",
- mediaSourceId: mediaSource?.Id ?? "",
+ subtitleIndex: subtitleIndex?.toString() ?? "",
+ mediaSourceId: mediaSourceIdRef.current ?? "",
bitrateValue: bitrate.toString(),
- playbackPosition: playbackPosition,
+ playbackPosition: playbackPositionRef.current,
}).toString();
router.replace(`player/direct-player?${queryParams}` as any);
},
- [item, mediaSource, subtitleIndex, audioIndex, playbackPosition],
+ [audioIndex, subtitleIndex, router],
);
- const handleSheetChanges = useCallback((index: number) => {
- if (index === -1) {
- setOpen(false);
- }
- }, []);
+ // Create stable identifiers for tracks
+ const subtitleTracksKey = useMemo(
+ () => subtitleTracks?.map((t) => `${t.index}-${t.name}`).join(",") ?? "",
+ [subtitleTracks],
+ );
- const renderBackdrop = useCallback(
- (props: BottomSheetBackdropProps) => (
-
+ const audioTracksKey = useMemo(
+ () => audioTracks?.map((t) => `${t.index}-${t.name}`).join(",") ?? "",
+ [audioTracks],
+ );
+
+ // Transform sections into OptionGroup format
+ const optionGroups = useMemo(() => {
+ const groups: OptionGroup[] = [];
+
+ // Quality Section
+ if (!isOffline) {
+ groups.push({
+ title: "Quality",
+ options:
+ BITRATES?.map((bitrate) => ({
+ type: "radio" as const,
+ label: bitrate.key,
+ value: bitrate.value?.toString() ?? "",
+ selected: bitrateValue === (bitrate.value?.toString() ?? ""),
+ onPress: () => changeBitrate(bitrate.value?.toString() ?? ""),
+ })) || [],
+ });
+ }
+
+ // Subtitle Section
+ if (subtitleTracks && subtitleTracks.length > 0) {
+ groups.push({
+ title: "Subtitles",
+ options: subtitleTracks.map((sub) => ({
+ type: "radio" as const,
+ label: sub.name,
+ value: sub.index.toString(),
+ selected: subtitleIndex === sub.index.toString(),
+ onPress: () => sub.setTrack(),
+ })),
+ });
+ }
+
+ // Audio Section
+ if (audioTracks && audioTracks.length > 0) {
+ groups.push({
+ title: "Audio",
+ options: audioTracks.map((track) => ({
+ type: "radio" as const,
+ label: track.name,
+ value: track.index.toString(),
+ selected: audioIndex === track.index.toString(),
+ onPress: () => track.setTrack(),
+ })),
+ });
+ }
+
+ return groups;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ isOffline,
+ bitrateValue,
+ changeBitrate,
+ subtitleTracksKey,
+ audioTracksKey,
+ subtitleIndex,
+ audioIndex,
+ // Note: subtitleTracks and audioTracks are intentionally excluded
+ // because we use subtitleTracksKey and audioTracksKey for stability
+ ]);
+
+ // Memoize the trigger to prevent re-renders
+ const trigger = useMemo(
+ () => (
+
+
+
),
[],
);
- 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 (
- <>
-
-
-
-
-
-
-
- Playback Options
-
- {/* Quality Section */}
- {!isOffline && (
-
-
- Quality
-
-
- {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 && (
-
- )}
-
- ))}
-
-
- )}
-
-
-
- >
+
);
};
diff --git a/components/video-player/controls/hooks/useRemoteControl.ts b/components/video-player/controls/hooks/useRemoteControl.ts
index b1df69fd..8eba2c45 100644
--- a/components/video-player/controls/hooks/useRemoteControl.ts
+++ b/components/video-player/controls/hooks/useRemoteControl.ts
@@ -1,9 +1,23 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { useTVEventHandler } from "react-native";
+import { Platform } from "react-native";
import { type SharedValue, useSharedValue } from "react-native-reanimated";
import { msToTicks, ticksToSeconds } from "@/utils/time";
import { CONTROLS_CONSTANTS } from "../constants";
+// TV event handler with fallback for non-TV platforms
+let useTVEventHandler: (callback: (evt: any) => void) => void;
+if (Platform.isTV) {
+ try {
+ useTVEventHandler = require("react-native").useTVEventHandler;
+ } catch {
+ // Fallback for non-TV platforms
+ useTVEventHandler = () => {};
+ }
+} else {
+ // No-op hook for non-TV platforms
+ useTVEventHandler = () => {};
+}
+
interface UseRemoteControlProps {
progress: SharedValue;
min: SharedValue;
@@ -63,6 +77,7 @@ export function useRemoteControl({
[isVlc],
);
+ // TV remote control handling (no-op on non-TV platforms)
useTVEventHandler((evt) => {
if (!evt) return;
diff --git a/components/video-player/controls/useControlsTimeout.ts b/components/video-player/controls/useControlsTimeout.ts
index d1e95b8c..80d41af2 100644
--- a/components/video-player/controls/useControlsTimeout.ts
+++ b/components/video-player/controls/useControlsTimeout.ts
@@ -13,7 +13,7 @@ export const useControlsTimeout = ({
isSliding,
episodeView,
onHideControls,
- timeout = 4000,
+ timeout = 10000,
}: UseControlsTimeoutProps) => {
const controlsTimeoutRef = useRef | null>(null);
diff --git a/docs/nested-modals.md b/docs/nested-modals.md
new file mode 100644
index 00000000..c0010fda
--- /dev/null
+++ b/docs/nested-modals.md
@@ -0,0 +1,58 @@
+# Nested Modals with PlatformDropdown
+
+## Issue
+PlatformDropdowns inside BottomSheetModals don't open on Android.
+
+## Solution
+1. **Add controlled state** for each PlatformDropdown:
+ ```tsx
+ const [open, setOpen] = useState(false);
+
+
+ ```
+
+2. **Use `View` for triggers, not `TouchableOpacity`**:
+ ```tsx
+ // ✅ Correct
+ ...}
+ />
+
+ // ❌ Wrong - causes nested TouchableOpacity conflicts
+ ...}
+ />
+ ```
+
+3. **Add `stackBehavior='push'` to parent BottomSheetModal**:
+ ```tsx
+
+ ```
+
+4. **Reset dropdown states on modal dismiss**:
+ ```tsx
+ const handleDismiss = useCallback(() => {
+ setDropdown1Open(false);
+ setDropdown2Open(false);
+ // reset all dropdown states
+ onDismiss?.();
+ }, [onDismiss]);
+
+
+ ```
+
+## Why
+- PlatformDropdown wraps triggers in TouchableOpacity on Android. Nested TouchableOpacity causes touch event conflicts.
+- PlatformDropdown's useEffect should only call `showModal()` when `open === true`, not call `hideModal()` when `open === false` (interferes with parent modals).
+- Dropdown states must be reset on modal dismiss to prevent them from reopening automatically when parent modal reopens.
+
diff --git a/eas.json b/eas.json
index e23dfbf5..9463c30f 100644
--- a/eas.json
+++ b/eas.json
@@ -45,14 +45,14 @@
},
"production": {
"environment": "production",
- "channel": "0.40.4",
+ "channel": "0.46.2",
"android": {
"image": "latest"
}
},
"production-apk": {
"environment": "production",
- "channel": "0.40.4",
+ "channel": "0.46.2",
"android": {
"buildType": "apk",
"image": "latest"
@@ -60,7 +60,7 @@
},
"production-apk-tv": {
"environment": "production",
- "channel": "0.40.4",
+ "channel": "0.46.2",
"android": {
"buildType": "apk",
"image": "latest"
diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts
index 14004596..b25551d3 100644
--- a/hooks/useIntroSkipper.ts
+++ b/hooks/useIntroSkipper.ts
@@ -43,26 +43,60 @@ export const useIntroSkipper = (
const introTimestamps = segments?.introSegments?.[0];
useEffect(() => {
+ console.log(`[INTRO_SKIPPER] Hook state:`, {
+ itemId,
+ currentTime,
+ hasSegments: !!segments,
+ segments: segments,
+ introSegmentsCount: segments?.introSegments?.length || 0,
+ introSegments: segments?.introSegments,
+ hasIntroTimestamps: !!introTimestamps,
+ introTimestamps,
+ isVlc,
+ isOffline,
+ });
+
if (introTimestamps) {
- setShowSkipButton(
+ const shouldShow =
currentTime > introTimestamps.startTime &&
- currentTime < introTimestamps.endTime,
- );
+ currentTime < introTimestamps.endTime;
+
+ console.log(`[INTRO_SKIPPER] Button visibility check:`, {
+ currentTime,
+ introStart: introTimestamps.startTime,
+ introEnd: introTimestamps.endTime,
+ afterStart: currentTime > introTimestamps.startTime,
+ beforeEnd: currentTime < introTimestamps.endTime,
+ shouldShow,
+ });
+
+ setShowSkipButton(shouldShow);
+ } else {
+ if (showSkipButton) {
+ console.log(`[INTRO_SKIPPER] No intro timestamps, hiding button`);
+ setShowSkipButton(false);
+ }
}
- }, [introTimestamps, currentTime]);
+ }, [introTimestamps, currentTime, showSkipButton]);
const skipIntro = useCallback(() => {
if (!introTimestamps) return;
try {
+ console.log(
+ `[INTRO_SKIPPER] Skipping intro to:`,
+ introTimestamps.endTime,
+ );
lightHapticFeedback();
wrappedSeek(introTimestamps.endTime);
setTimeout(() => {
play();
}, 200);
} catch (error) {
- console.error("Error skipping intro", error);
+ console.error("[INTRO_SKIPPER] Error skipping intro", error);
}
}, [introTimestamps, lightHapticFeedback, wrappedSeek, play]);
+ console.log(`[INTRO_SKIPPER] Returning state:`, { showSkipButton });
+
return { showSkipButton, skipIntro };
};
diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts
index f855b332..34cc4dfe 100644
--- a/hooks/useJellyseerr.ts
+++ b/hooks/useJellyseerr.ts
@@ -66,8 +66,8 @@ const JELLYSEERR_USER = "JELLYSEERR_USER";
const JELLYSEERR_COOKIES = "JELLYSEERR_COOKIES";
export const clearJellyseerrStorageData = () => {
- storage.delete(JELLYSEERR_USER);
- storage.delete(JELLYSEERR_COOKIES);
+ storage.remove(JELLYSEERR_USER);
+ storage.remove(JELLYSEERR_COOKIES);
};
export enum Endpoints {
diff --git a/hooks/useOrientation.ts b/hooks/useOrientation.ts
index 80a01ffb..62270c4f 100644
--- a/hooks/useOrientation.ts
+++ b/hooks/useOrientation.ts
@@ -1,7 +1,23 @@
import { useEffect, useState } from "react";
import { Platform } from "react-native";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
-import orientationToOrientationLock from "@/utils/OrientationLockConverter";
+import { OrientationLock } from "@/packages/expo-screen-orientation";
+import { Orientation } from "../packages/expo-screen-orientation.tv";
+
+const orientationToOrientationLock = (
+ orientation: Orientation,
+): OrientationLock => {
+ switch (orientation) {
+ case Orientation.LANDSCAPE_LEFT:
+ return OrientationLock.LANDSCAPE_LEFT;
+ case Orientation.LANDSCAPE_RIGHT:
+ return OrientationLock.LANDSCAPE_RIGHT;
+ case Orientation.PORTRAIT_UP:
+ return OrientationLock.PORTRAIT_UP;
+ default:
+ return OrientationLock.PORTRAIT_UP;
+ }
+};
export const useOrientation = () => {
const [orientation, setOrientation] = useState(
@@ -29,5 +45,20 @@ export const useOrientation = () => {
};
}, []);
- return { orientation, setOrientation };
+ const lockOrientation = async (lock: OrientationLock) => {
+ if (Platform.isTV) return;
+
+ if (lock === ScreenOrientation.OrientationLock.DEFAULT) {
+ await ScreenOrientation.unlockAsync();
+ } else {
+ await ScreenOrientation.lockAsync(lock);
+ }
+ };
+
+ const unlockOrientation = async () => {
+ if (Platform.isTV) return;
+ await ScreenOrientation.unlockAsync();
+ };
+
+ return { orientation, setOrientation, lockOrientation, unlockOrientation };
};
diff --git a/metro.config.js b/metro.config.js
index 2e9a0e06..ff7d87fb 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -2,7 +2,7 @@
const { getDefaultConfig } = require("expo/metro-config");
/** @type {import('expo/metro-config').MetroConfig} */
-const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef
+const config = getDefaultConfig(__dirname);
// Add Hermes parser
config.transformer.hermesParser = true;
diff --git a/modules/background-downloader/README.md b/modules/background-downloader/README.md
new file mode 100644
index 00000000..53e419b5
--- /dev/null
+++ b/modules/background-downloader/README.md
@@ -0,0 +1,258 @@
+# Background Downloader Module
+
+A native iOS and Android module for downloading large files in the background using `NSURLSession` (iOS) and `DownloadManager` (Android).
+
+## Features
+
+- **Background Downloads**: Downloads continue even when the app is backgrounded or suspended
+- **Progress Tracking**: Real-time progress updates via events
+- **Multiple Downloads**: Support for concurrent downloads
+- **Cancellation**: Cancel individual or all downloads
+- **Custom Destination**: Optionally specify custom file paths
+- **Error Handling**: Comprehensive error reporting
+- **Cross-Platform**: Works on both iOS and Android
+
+## Usage
+
+### Basic Example
+
+```typescript
+import { BackgroundDownloader } from '@/modules';
+
+// Start a download
+const taskId = await BackgroundDownloader.startDownload(
+ 'https://example.com/largefile.mp4'
+);
+
+// Listen for progress updates
+const progressSub = BackgroundDownloader.addProgressListener((event) => {
+ console.log(`Progress: ${Math.floor(event.progress * 100)}%`);
+ console.log(`Downloaded: ${event.bytesWritten} / ${event.totalBytes}`);
+});
+
+// Listen for completion
+const completeSub = BackgroundDownloader.addCompleteListener((event) => {
+ console.log('Download complete!');
+ console.log('File saved to:', event.filePath);
+ console.log('Task ID:', event.taskId);
+});
+
+// Listen for errors
+const errorSub = BackgroundDownloader.addErrorListener((event) => {
+ console.error('Download failed:', event.error);
+});
+
+// Cancel a download
+BackgroundDownloader.cancelDownload(taskId);
+
+// Get all active downloads
+const activeDownloads = await BackgroundDownloader.getActiveDownloads();
+
+// Cleanup listeners when done
+progressSub.remove();
+completeSub.remove();
+errorSub.remove();
+```
+
+### Custom Destination Path
+
+```typescript
+import { BackgroundDownloader } from '@/modules';
+import * as FileSystem from 'expo-file-system';
+
+const destinationPath = `${FileSystem.documentDirectory}myfile.mp4`;
+const taskId = await BackgroundDownloader.startDownload(
+ 'https://example.com/video.mp4',
+ destinationPath
+);
+```
+
+### Managing Multiple Downloads
+
+```typescript
+import { BackgroundDownloader } from '@/modules';
+
+const downloads = new Map();
+
+async function startMultipleDownloads(urls: string[]) {
+ for (const url of urls) {
+ const taskId = await BackgroundDownloader.startDownload(url);
+ downloads.set(taskId, { url, progress: 0 });
+ }
+}
+
+// Track progress for each download
+const progressSub = BackgroundDownloader.addProgressListener((event) => {
+ const download = downloads.get(event.taskId);
+ if (download) {
+ download.progress = event.progress;
+ }
+});
+
+// Cancel all downloads
+BackgroundDownloader.cancelAllDownloads();
+```
+
+## API Reference
+
+### Methods
+
+#### `startDownload(url: string, destinationPath?: string): Promise`
+
+Starts a new background download.
+
+- **Parameters:**
+ - `url`: The URL of the file to download
+ - `destinationPath`: (Optional) Custom file path for the downloaded file
+- **Returns:** Promise that resolves to the task ID (number)
+
+#### `cancelDownload(taskId: number): void`
+
+Cancels a specific download by task ID.
+
+- **Parameters:**
+ - `taskId`: The task ID returned by `startDownload`
+
+#### `cancelAllDownloads(): void`
+
+Cancels all active downloads.
+
+#### `getActiveDownloads(): Promise`
+
+Gets information about all active downloads.
+
+- **Returns:** Promise that resolves to an array of active downloads
+
+### Event Listeners
+
+#### `addProgressListener(listener: (event: DownloadProgressEvent) => void): Subscription`
+
+Listens for download progress updates.
+
+- **Event payload:**
+ - `taskId`: number
+ - `bytesWritten`: number
+ - `totalBytes`: number
+ - `progress`: number (0.0 to 1.0)
+
+#### `addCompleteListener(listener: (event: DownloadCompleteEvent) => void): Subscription`
+
+Listens for download completion.
+
+- **Event payload:**
+ - `taskId`: number
+ - `filePath`: string
+ - `url`: string
+
+#### `addErrorListener(listener: (event: DownloadErrorEvent) => void): Subscription`
+
+Listens for download errors.
+
+- **Event payload:**
+ - `taskId`: number
+ - `error`: string
+
+#### `addStartedListener(listener: (event: DownloadStartedEvent) => void): Subscription`
+
+Listens for download start confirmation.
+
+- **Event payload:**
+ - `taskId`: number
+ - `url`: string
+
+## Types
+
+```typescript
+interface DownloadProgressEvent {
+ taskId: number;
+ bytesWritten: number;
+ totalBytes: number;
+ progress: number;
+}
+
+interface DownloadCompleteEvent {
+ taskId: number;
+ filePath: string;
+ url: string;
+}
+
+interface DownloadErrorEvent {
+ taskId: number;
+ error: string;
+}
+
+interface DownloadStartedEvent {
+ taskId: number;
+ url: string;
+}
+
+interface ActiveDownload {
+ taskId: number;
+ url: string;
+ state: 'running' | 'suspended' | 'canceling' | 'completed' | 'unknown';
+}
+```
+
+## Implementation Details
+
+### iOS Background Downloads
+
+- Uses `NSURLSession` with background configuration
+- Session identifier: `com.fredrikburmester.streamyfin.backgrounddownloader`
+- Downloads continue when app is backgrounded or suspended
+- System may terminate downloads if app is force-quit
+
+### Android Background Downloads
+
+- Uses Android's `DownloadManager` API
+- Downloads are managed by the system and continue in the background
+- Shows download notification in the notification tray
+- Downloads continue even if the app is closed
+- Requires `INTERNET` permission (automatically added by Expo)
+
+### Background Modes
+
+The app's `Info.plist` already includes the required background mode for iOS:
+
+- `UIBackgroundModes`: `["audio", "fetch"]`
+
+### File Storage
+
+**iOS:** By default, downloaded files are saved to the app's Documents directory.
+
+**Android:** By default, files are saved to the app's external files directory (accessible via `FileSystem.documentDirectory` in Expo).
+
+You can specify a custom path using the `destinationPath` parameter on both platforms.
+
+## Building
+
+After adding this module, rebuild the app:
+
+```bash
+# iOS
+npx expo prebuild -p ios
+npx expo run:ios
+
+# Android
+npx expo prebuild -p android
+npx expo run:android
+```
+
+Or install manually:
+
+```bash
+# iOS
+cd ios
+pod install
+cd ..
+
+# Android - prebuild handles everything
+npx expo prebuild -p android
+```
+
+## Notes
+
+- Background downloads may be cancelled if the user force-quits the app (iOS)
+- The OS manages download priority and may pause downloads to save battery
+- Android shows a system notification for ongoing downloads
+- Downloads over cellular are allowed by default on both platforms
diff --git a/modules/background-downloader/android/build.gradle b/modules/background-downloader/android/build.gradle
new file mode 100644
index 00000000..1b273d72
--- /dev/null
+++ b/modules/background-downloader/android/build.gradle
@@ -0,0 +1,46 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+group = 'expo.modules.backgrounddownloader'
+version = '1.0.0'
+
+def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
+def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
+
+apply from: expoModulesCorePlugin
+
+applyKotlinExpoModulesCorePlugin()
+useDefaultAndroidSdkVersions()
+useCoreDependencies()
+useExpoPublishing()
+
+android {
+ namespace "expo.modules.backgrounddownloader"
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+}
+
diff --git a/modules/background-downloader/android/src/main/AndroidManifest.xml b/modules/background-downloader/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..44554032
--- /dev/null
+++ b/modules/background-downloader/android/src/main/AndroidManifest.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/BackgroundDownloaderModule.kt b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/BackgroundDownloaderModule.kt
new file mode 100644
index 00000000..a2913b20
--- /dev/null
+++ b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/BackgroundDownloaderModule.kt
@@ -0,0 +1,300 @@
+package expo.modules.backgrounddownloader
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
+import expo.modules.kotlin.Promise
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+
+data class DownloadTaskInfo(
+ val url: String,
+ val destinationPath: String?
+)
+
+class BackgroundDownloaderModule : Module() {
+ companion object {
+ private const val TAG = "BackgroundDownloader"
+ }
+
+ private val context
+ get() = requireNotNull(appContext.reactContext)
+
+ private val downloadManager = OkHttpDownloadManager()
+ private val downloadTasks = mutableMapOf()
+ private val downloadQueue = mutableListOf>()
+ private var taskIdCounter = 1
+ private var downloadService: DownloadService? = null
+ private var serviceBound = false
+
+ private val serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ Log.d(TAG, "Service connected")
+ val binder = service as DownloadService.DownloadServiceBinder
+ downloadService = binder.getService()
+ serviceBound = true
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ Log.d(TAG, "Service disconnected")
+ downloadService = null
+ serviceBound = false
+ }
+ }
+
+ override fun definition() = ModuleDefinition {
+ Name("BackgroundDownloader")
+
+ Events(
+ "onDownloadProgress",
+ "onDownloadComplete",
+ "onDownloadError",
+ "onDownloadStarted"
+ )
+
+ OnCreate {
+ Log.d(TAG, "Module created")
+ }
+
+ OnDestroy {
+ Log.d(TAG, "Module destroyed")
+ downloadManager.cancelAllDownloads()
+ if (serviceBound) {
+ try {
+ context.unbindService(serviceConnection)
+ serviceBound = false
+ } catch (e: Exception) {
+ Log.e(TAG, "Error unbinding service: ${e.message}")
+ }
+ }
+ }
+
+ AsyncFunction("startDownload") { urlString: String, destinationPath: String?, promise: Promise ->
+ try {
+ val taskId = startDownloadInternal(urlString, destinationPath)
+ promise.resolve(taskId)
+ } catch (e: Exception) {
+ promise.reject("DOWNLOAD_ERROR", "Failed to start download: ${e.message}", e)
+ }
+ }
+
+ AsyncFunction("enqueueDownload") { urlString: String, destinationPath: String?, promise: Promise ->
+ try {
+ Log.d(TAG, "Enqueuing download: url=$urlString")
+
+ // Add to queue
+ val wasEmpty = downloadQueue.isEmpty()
+ downloadQueue.add(Pair(urlString, destinationPath))
+ Log.d(TAG, "Queue size: ${downloadQueue.size}")
+
+ // If queue was empty and no active downloads, start processing immediately
+ if (wasEmpty && downloadTasks.isEmpty()) {
+ val taskId = processNextInQueue()
+ promise.resolve(taskId)
+ } else {
+ // Return placeholder taskId for queued items
+ promise.resolve(-1)
+ }
+ } catch (e: Exception) {
+ promise.reject("DOWNLOAD_ERROR", "Failed to enqueue download: ${e.message}", e)
+ }
+ }
+
+ Function("cancelDownload") { taskId: Int ->
+ Log.d(TAG, "Cancelling download: taskId=$taskId")
+ downloadManager.cancelDownload(taskId)
+ downloadTasks.remove(taskId)
+ downloadService?.stopDownload()
+
+ // Process next item in queue after cancellation
+ processNextInQueue()
+ }
+
+ Function("cancelQueuedDownload") { url: String ->
+ // Remove from queue by URL
+ downloadQueue.removeAll { queuedItem ->
+ queuedItem.first == url
+ }
+ Log.d(TAG, "Removed queued download: $url, queue size: ${downloadQueue.size}")
+ }
+
+ Function("cancelAllDownloads") {
+ Log.d(TAG, "Cancelling all downloads")
+ downloadManager.cancelAllDownloads()
+ downloadTasks.clear()
+ downloadQueue.clear()
+ stopDownloadService()
+ }
+
+ AsyncFunction("getActiveDownloads") { promise: Promise ->
+ try {
+ val activeDownloads = downloadTasks.map { (taskId, taskInfo) ->
+ mapOf(
+ "taskId" to taskId,
+ "url" to taskInfo.url
+ )
+ }
+ promise.resolve(activeDownloads)
+ } catch (e: Exception) {
+ promise.reject("ERROR", "Failed to get active downloads: ${e.message}", e)
+ }
+ }
+ }
+
+ private fun startDownloadInternal(urlString: String, destinationPath: String?): Int {
+ val taskId = taskIdCounter++
+
+ if (destinationPath == null) {
+ throw IllegalArgumentException("Destination path is required")
+ }
+
+ downloadTasks[taskId] = DownloadTaskInfo(
+ url = urlString,
+ destinationPath = destinationPath
+ )
+
+ // Start foreground service if not running
+ startDownloadService()
+ downloadService?.startDownload()
+
+ Log.d(TAG, "Starting download: taskId=$taskId, url=$urlString")
+
+ // Send started event
+ sendEvent("onDownloadStarted", mapOf(
+ "taskId" to taskId,
+ "url" to urlString
+ ))
+
+ // Start the download with OkHttp
+ downloadManager.startDownload(
+ taskId = taskId,
+ url = urlString,
+ destinationPath = destinationPath,
+ onProgress = { bytesWritten, totalBytes ->
+ handleProgress(taskId, bytesWritten, totalBytes)
+ },
+ onComplete = { filePath ->
+ handleDownloadComplete(taskId, filePath)
+ },
+ onError = { error ->
+ handleError(taskId, error)
+ }
+ )
+
+ return taskId
+ }
+
+ private fun processNextInQueue(): Int {
+ // Check if queue has items
+ if (downloadQueue.isEmpty()) {
+ Log.d(TAG, "Queue is empty")
+ return -1
+ }
+
+ // Check if there are active downloads (one at a time)
+ if (downloadTasks.isNotEmpty()) {
+ Log.d(TAG, "Active downloads in progress (${downloadTasks.size}), waiting...")
+ return -1
+ }
+
+ // Get next item from queue
+ val (url, destinationPath) = downloadQueue.removeAt(0)
+ Log.d(TAG, "Processing next in queue: $url")
+
+ return try {
+ startDownloadInternal(url, destinationPath)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error processing queue item: ${e.message}", e)
+ // Try to process next item
+ processNextInQueue()
+ }
+ }
+
+ private fun handleProgress(taskId: Int, bytesWritten: Long, totalBytes: Long) {
+ val progress = if (totalBytes > 0) {
+ bytesWritten.toDouble() / totalBytes.toDouble()
+ } else {
+ 0.0
+ }
+
+ // Update notification
+ val taskInfo = downloadTasks[taskId]
+ if (taskInfo != null) {
+ val progressPercent = (progress * 100).toInt()
+ downloadService?.updateProgress("Downloading video", progressPercent)
+ }
+
+ sendEvent("onDownloadProgress", mapOf(
+ "taskId" to taskId,
+ "bytesWritten" to bytesWritten,
+ "totalBytes" to totalBytes,
+ "progress" to progress
+ ))
+ }
+
+ private fun handleDownloadComplete(taskId: Int, filePath: String) {
+ val taskInfo = downloadTasks[taskId]
+
+ if (taskInfo == null) {
+ Log.e(TAG, "Download completed but task info not found: taskId=$taskId")
+ return
+ }
+
+ Log.d(TAG, "Download completed: taskId=$taskId, filePath=$filePath")
+
+ sendEvent("onDownloadComplete", mapOf(
+ "taskId" to taskId,
+ "filePath" to filePath,
+ "url" to taskInfo.url
+ ))
+
+ downloadTasks.remove(taskId)
+ downloadService?.stopDownload()
+
+ // Process next item in queue
+ processNextInQueue()
+ }
+
+ private fun handleError(taskId: Int, error: String) {
+ val taskInfo = downloadTasks[taskId]
+
+ Log.e(TAG, "Download error: taskId=$taskId, error=$error")
+
+ sendEvent("onDownloadError", mapOf(
+ "taskId" to taskId,
+ "error" to error
+ ))
+
+ downloadTasks.remove(taskId)
+ downloadService?.stopDownload()
+
+ // Process next item in queue even on error
+ processNextInQueue()
+ }
+
+ private fun startDownloadService() {
+ if (!serviceBound) {
+ val intent = Intent(context, DownloadService::class.java)
+ context.startForegroundService(intent)
+ context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+ }
+
+ private fun stopDownloadService() {
+ if (serviceBound && downloadTasks.isEmpty()) {
+ try {
+ context.unbindService(serviceConnection)
+ serviceBound = false
+ downloadService = null
+
+ val intent = Intent(context, DownloadService::class.java)
+ context.stopService(intent)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error stopping service: ${e.message}")
+ }
+ }
+ }
+}
diff --git a/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/DownloadService.kt b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/DownloadService.kt
new file mode 100644
index 00000000..ed05fc5c
--- /dev/null
+++ b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/DownloadService.kt
@@ -0,0 +1,111 @@
+package expo.modules.backgrounddownloader
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+
+class DownloadService : Service() {
+ private val TAG = "DownloadService"
+ private val NOTIFICATION_ID = 1001
+ private val CHANNEL_ID = "download_channel"
+
+ private val binder = DownloadServiceBinder()
+ private var activeDownloadCount = 0
+ private var currentDownloadTitle = "Preparing download..."
+ private var currentProgress = 0
+
+ inner class DownloadServiceBinder : Binder() {
+ fun getService(): DownloadService = this@DownloadService
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d(TAG, "DownloadService created")
+ createNotificationChannel()
+ }
+
+ override fun onBind(intent: Intent?): IBinder {
+ Log.d(TAG, "DownloadService bound")
+ return binder
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(TAG, "DownloadService started")
+ startForeground(NOTIFICATION_ID, createNotification())
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "DownloadService destroyed")
+ super.onDestroy()
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "Downloads",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Video download progress"
+ setShowBadge(false)
+ }
+
+ val notificationManager = getSystemService(NotificationManager::class.java)
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createNotification(): Notification {
+ val builder = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(currentDownloadTitle)
+ .setSmallIcon(android.R.drawable.stat_sys_download)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
+ .setOnlyAlertOnce(true)
+
+ if (currentProgress > 0) {
+ builder.setProgress(100, currentProgress, false)
+ .setContentText("$currentProgress% complete")
+ } else {
+ builder.setProgress(100, 0, true)
+ .setContentText("Starting...")
+ }
+
+ return builder.build()
+ }
+
+ fun startDownload() {
+ activeDownloadCount++
+ Log.d(TAG, "Download started, active count: $activeDownloadCount")
+ if (activeDownloadCount == 1) {
+ startForeground(NOTIFICATION_ID, createNotification())
+ }
+ }
+
+ fun stopDownload() {
+ activeDownloadCount = maxOf(0, activeDownloadCount - 1)
+ Log.d(TAG, "Download stopped, active count: $activeDownloadCount")
+ if (activeDownloadCount == 0) {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+ }
+
+ fun updateProgress(title: String, progress: Int) {
+ currentDownloadTitle = title
+ currentProgress = progress
+
+ val notificationManager = getSystemService(NotificationManager::class.java)
+ notificationManager.notify(NOTIFICATION_ID, createNotification())
+ }
+}
+
+
diff --git a/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/OkHttpDownloadManager.kt b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/OkHttpDownloadManager.kt
new file mode 100644
index 00000000..f5d1f1a1
--- /dev/null
+++ b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/OkHttpDownloadManager.kt
@@ -0,0 +1,151 @@
+package expo.modules.backgrounddownloader
+
+import android.util.Log
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+class OkHttpDownloadManager {
+ private val TAG = "OkHttpDownloadManager"
+
+ private val client = OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.SECONDS)
+ .callTimeout(0, TimeUnit.SECONDS) // No timeout for long transcodes
+ .build()
+
+ private val activeDownloads = mutableMapOf()
+
+ fun startDownload(
+ taskId: Int,
+ url: String,
+ destinationPath: String,
+ onProgress: (bytesWritten: Long, totalBytes: Long) -> Unit,
+ onComplete: (filePath: String) -> Unit,
+ onError: (error: String) -> Unit
+ ) {
+ Log.d(TAG, "Starting download: taskId=$taskId, url=$url")
+
+ val request = Request.Builder()
+ .url(url)
+ .build()
+
+ val call = client.newCall(request)
+ activeDownloads[taskId] = call
+
+ call.enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ Log.e(TAG, "Download failed: taskId=$taskId, error=${e.message}")
+ activeDownloads.remove(taskId)
+ if (call.isCanceled()) {
+ // Don't report cancellation as error
+ return
+ }
+ onError(e.message ?: "Download failed")
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ if (!response.isSuccessful) {
+ Log.e(TAG, "Download failed with HTTP code: ${response.code}")
+ activeDownloads.remove(taskId)
+ onError("HTTP error: ${response.code} ${response.message}")
+ return
+ }
+
+ try {
+ val totalBytes = response.body?.contentLength() ?: -1L
+ val inputStream = response.body?.byteStream()
+
+ if (inputStream == null) {
+ activeDownloads.remove(taskId)
+ onError("Failed to get response body")
+ return
+ }
+
+ // Create destination directory if needed
+ val destFile = File(destinationPath)
+ val destDir = destFile.parentFile
+ if (destDir != null && !destDir.exists()) {
+ destDir.mkdirs()
+ }
+
+ val outputStream = destFile.outputStream()
+ val buffer = ByteArray(8192)
+ var bytesWritten = 0L
+ var lastProgressUpdate = System.currentTimeMillis()
+
+ inputStream.use { input ->
+ outputStream.use { output ->
+ var bytes = input.read(buffer)
+ while (bytes >= 0) {
+ // Check if download was cancelled
+ if (call.isCanceled()) {
+ Log.d(TAG, "Download cancelled: taskId=$taskId")
+ destFile.delete()
+ activeDownloads.remove(taskId)
+ return
+ }
+
+ output.write(buffer, 0, bytes)
+ bytesWritten += bytes
+
+ // Throttle progress updates to every 500ms
+ val now = System.currentTimeMillis()
+ if (now - lastProgressUpdate >= 500) {
+ onProgress(bytesWritten, totalBytes)
+ lastProgressUpdate = now
+ }
+
+ bytes = input.read(buffer)
+ }
+ }
+ }
+
+ // Send final progress update
+ onProgress(bytesWritten, totalBytes)
+
+ Log.d(TAG, "Download completed: taskId=$taskId, bytes=$bytesWritten")
+ activeDownloads.remove(taskId)
+ onComplete(destinationPath)
+
+ } catch (e: Exception) {
+ Log.e(TAG, "Error during download: taskId=$taskId, error=${e.message}", e)
+ activeDownloads.remove(taskId)
+
+ // Clean up partial file
+ try {
+ File(destinationPath).delete()
+ } catch (deleteError: Exception) {
+ Log.e(TAG, "Failed to delete partial file: ${deleteError.message}")
+ }
+
+ if (!call.isCanceled()) {
+ onError(e.message ?: "Download failed")
+ }
+ }
+ }
+ })
+ }
+
+ fun cancelDownload(taskId: Int) {
+ Log.d(TAG, "Cancelling download: taskId=$taskId")
+ activeDownloads[taskId]?.cancel()
+ activeDownloads.remove(taskId)
+ }
+
+ fun cancelAllDownloads() {
+ Log.d(TAG, "Cancelling all downloads")
+ activeDownloads.values.forEach { it.cancel() }
+ activeDownloads.clear()
+ }
+
+ fun hasActiveDownloads(): Boolean {
+ return activeDownloads.isNotEmpty()
+ }
+}
+
diff --git a/modules/background-downloader/example.ts b/modules/background-downloader/example.ts
new file mode 100644
index 00000000..51dd15af
--- /dev/null
+++ b/modules/background-downloader/example.ts
@@ -0,0 +1,98 @@
+import type {
+ DownloadCompleteEvent,
+ DownloadErrorEvent,
+ DownloadProgressEvent,
+} from "@/modules";
+import { BackgroundDownloader } from "@/modules";
+
+export class DownloadManager {
+ private progressSubscription: any;
+ private completeSubscription: any;
+ private errorSubscription: any;
+ private activeDownloads = new Map<
+ number,
+ { url: string; progress: number }
+ >();
+
+ constructor() {
+ this.setupListeners();
+ }
+
+ private setupListeners() {
+ this.progressSubscription = BackgroundDownloader.addProgressListener(
+ (event: DownloadProgressEvent) => {
+ const download = this.activeDownloads.get(event.taskId);
+ if (download) {
+ download.progress = event.progress;
+ console.log(
+ `Download ${event.taskId}: ${Math.floor(event.progress * 100)}%`,
+ );
+ }
+ },
+ );
+
+ this.completeSubscription = BackgroundDownloader.addCompleteListener(
+ (event: DownloadCompleteEvent) => {
+ console.log("Download complete:", event.filePath);
+ this.activeDownloads.delete(event.taskId);
+ },
+ );
+
+ this.errorSubscription = BackgroundDownloader.addErrorListener(
+ (event: DownloadErrorEvent) => {
+ console.error("Download error:", event.error);
+ this.activeDownloads.delete(event.taskId);
+ },
+ );
+ }
+
+ async startDownload(url: string, destinationPath?: string): Promise {
+ const taskId = await BackgroundDownloader.startDownload(
+ url,
+ destinationPath,
+ );
+ this.activeDownloads.set(taskId, { url, progress: 0 });
+ return taskId;
+ }
+
+ cancelDownload(taskId: number): void {
+ BackgroundDownloader.cancelDownload(taskId);
+ this.activeDownloads.delete(taskId);
+ }
+
+ cancelAllDownloads(): void {
+ BackgroundDownloader.cancelAllDownloads();
+ this.activeDownloads.clear();
+ }
+
+ async getActiveDownloads() {
+ return await BackgroundDownloader.getActiveDownloads();
+ }
+
+ cleanup(): void {
+ this.progressSubscription?.remove();
+ this.completeSubscription?.remove();
+ this.errorSubscription?.remove();
+ }
+}
+
+const downloadManager = new DownloadManager();
+
+export async function downloadFile(
+ url: string,
+ destinationPath?: string,
+): Promise {
+ return await downloadManager.startDownload(url, destinationPath);
+}
+
+export function cancelDownload(taskId: number): void {
+ downloadManager.cancelDownload(taskId);
+}
+
+export function cancelAllDownloads(): void {
+ downloadManager.cancelAllDownloads();
+}
+
+export async function getActiveDownloads() {
+ return await downloadManager.getActiveDownloads();
+}
diff --git a/modules/background-downloader/expo-module.config.json b/modules/background-downloader/expo-module.config.json
new file mode 100644
index 00000000..f588b229
--- /dev/null
+++ b/modules/background-downloader/expo-module.config.json
@@ -0,0 +1,12 @@
+{
+ "name": "background-downloader",
+ "version": "1.0.0",
+ "platforms": ["ios", "android"],
+ "ios": {
+ "modules": ["BackgroundDownloaderModule"],
+ "appDelegateSubscribers": ["BackgroundDownloaderAppDelegate"]
+ },
+ "android": {
+ "modules": ["expo.modules.backgrounddownloader.BackgroundDownloaderModule"]
+ }
+}
diff --git a/modules/background-downloader/index.ts b/modules/background-downloader/index.ts
new file mode 100644
index 00000000..93acce01
--- /dev/null
+++ b/modules/background-downloader/index.ts
@@ -0,0 +1,109 @@
+import type { EventSubscription } from "expo-modules-core";
+import type {
+ ActiveDownload,
+ DownloadCompleteEvent,
+ DownloadErrorEvent,
+ DownloadProgressEvent,
+ DownloadStartedEvent,
+} from "./src/BackgroundDownloader.types";
+import BackgroundDownloaderModule from "./src/BackgroundDownloaderModule";
+
+export interface BackgroundDownloader {
+ startDownload(url: string, destinationPath?: string): Promise;
+ enqueueDownload(url: string, destinationPath?: string): Promise;
+ cancelDownload(taskId: number): void;
+ cancelQueuedDownload(url: string): void;
+ cancelAllDownloads(): void;
+ getActiveDownloads(): Promise;
+
+ addProgressListener(
+ listener: (event: DownloadProgressEvent) => void,
+ ): EventSubscription;
+
+ addCompleteListener(
+ listener: (event: DownloadCompleteEvent) => void,
+ ): EventSubscription;
+
+ addErrorListener(
+ listener: (event: DownloadErrorEvent) => void,
+ ): EventSubscription;
+
+ addStartedListener(
+ listener: (event: DownloadStartedEvent) => void,
+ ): EventSubscription;
+}
+
+const BackgroundDownloader: BackgroundDownloader = {
+ async startDownload(url: string, destinationPath?: string): Promise {
+ return await BackgroundDownloaderModule.startDownload(url, destinationPath);
+ },
+
+ async enqueueDownload(
+ url: string,
+ destinationPath?: string,
+ ): Promise {
+ return await BackgroundDownloaderModule.enqueueDownload(
+ url,
+ destinationPath,
+ );
+ },
+
+ cancelDownload(taskId: number): void {
+ BackgroundDownloaderModule.cancelDownload(taskId);
+ },
+
+ cancelQueuedDownload(url: string): void {
+ BackgroundDownloaderModule.cancelQueuedDownload(url);
+ },
+
+ cancelAllDownloads(): void {
+ BackgroundDownloaderModule.cancelAllDownloads();
+ },
+
+ async getActiveDownloads(): Promise {
+ return await BackgroundDownloaderModule.getActiveDownloads();
+ },
+
+ addProgressListener(
+ listener: (event: DownloadProgressEvent) => void,
+ ): EventSubscription {
+ return BackgroundDownloaderModule.addListener(
+ "onDownloadProgress",
+ listener,
+ );
+ },
+
+ addCompleteListener(
+ listener: (event: DownloadCompleteEvent) => void,
+ ): EventSubscription {
+ return BackgroundDownloaderModule.addListener(
+ "onDownloadComplete",
+ listener,
+ );
+ },
+
+ addErrorListener(
+ listener: (event: DownloadErrorEvent) => void,
+ ): EventSubscription {
+ return BackgroundDownloaderModule.addListener("onDownloadError", listener);
+ },
+
+ addStartedListener(
+ listener: (event: DownloadStartedEvent) => void,
+ ): EventSubscription {
+ return BackgroundDownloaderModule.addListener(
+ "onDownloadStarted",
+ listener,
+ );
+ },
+};
+
+export default BackgroundDownloader;
+
+export type {
+ ActiveDownload,
+ DownloadCompleteEvent,
+ DownloadErrorEvent,
+ DownloadProgressEvent,
+ DownloadStartedEvent,
+};
diff --git a/modules/background-downloader/ios/BackgroundDownloader.podspec b/modules/background-downloader/ios/BackgroundDownloader.podspec
new file mode 100644
index 00000000..b1d778a1
--- /dev/null
+++ b/modules/background-downloader/ios/BackgroundDownloader.podspec
@@ -0,0 +1,21 @@
+Pod::Spec.new do |s|
+ s.name = 'BackgroundDownloader'
+ s.version = '1.0.0'
+ s.summary = 'Background file downloader for iOS'
+ s.description = 'Native iOS module for downloading large files in the background using NSURLSession'
+ s.author = ''
+ s.homepage = 'https://docs.expo.dev/modules/'
+ s.platforms = { :ios => '15.6', :tvos => '15.0' }
+ s.source = { git: '' }
+ s.static_framework = true
+
+ s.dependency 'ExpoModulesCore'
+
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
+ }
+
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
+end
+
diff --git a/modules/background-downloader/ios/BackgroundDownloaderAppDelegate.swift b/modules/background-downloader/ios/BackgroundDownloaderAppDelegate.swift
new file mode 100644
index 00000000..d34f97ff
--- /dev/null
+++ b/modules/background-downloader/ios/BackgroundDownloaderAppDelegate.swift
@@ -0,0 +1,15 @@
+import ExpoModulesCore
+import UIKit
+
+public class BackgroundDownloaderAppDelegate: ExpoAppDelegateSubscriber {
+ public func application(
+ _ application: UIApplication,
+ handleEventsForBackgroundURLSession identifier: String,
+ completionHandler: @escaping () -> Void
+ ) {
+ if identifier == "com.fredrikburmester.streamyfin.backgrounddownloader" {
+ BackgroundDownloaderModule.setBackgroundCompletionHandler(completionHandler)
+ }
+ }
+}
+
diff --git a/modules/background-downloader/ios/BackgroundDownloaderModule.swift b/modules/background-downloader/ios/BackgroundDownloaderModule.swift
new file mode 100644
index 00000000..4e50c723
--- /dev/null
+++ b/modules/background-downloader/ios/BackgroundDownloaderModule.swift
@@ -0,0 +1,397 @@
+import ExpoModulesCore
+import Foundation
+
+enum DownloadError: Error {
+ case invalidURL
+ case fileOperationFailed
+ case downloadFailed
+}
+
+struct DownloadTaskInfo {
+ let url: String
+ let destinationPath: String?
+}
+
+// Separate delegate class to handle URLSession callbacks
+class DownloadSessionDelegate: NSObject, URLSessionDownloadDelegate {
+ weak var module: BackgroundDownloaderModule?
+
+ init(module: BackgroundDownloaderModule) {
+ self.module = module
+ super.init()
+ }
+
+ func urlSession(
+ _ session: URLSession,
+ downloadTask: URLSessionDownloadTask,
+ didWriteData bytesWritten: Int64,
+ totalBytesWritten: Int64,
+ totalBytesExpectedToWrite: Int64
+ ) {
+ module?.handleProgress(
+ taskId: downloadTask.taskIdentifier,
+ bytesWritten: totalBytesWritten,
+ totalBytes: totalBytesExpectedToWrite
+ )
+ }
+
+ func urlSession(
+ _ session: URLSession,
+ downloadTask: URLSessionDownloadTask,
+ didFinishDownloadingTo location: URL
+ ) {
+ module?.handleDownloadComplete(
+ taskId: downloadTask.taskIdentifier,
+ location: location,
+ downloadTask: downloadTask
+ )
+ }
+
+ func urlSession(
+ _ session: URLSession,
+ task: URLSessionTask,
+ didCompleteWithError error: Error?
+ ) {
+ if let error = error {
+ print("[BackgroundDownloader] Task \(task.taskIdentifier) error: \(error.localizedDescription)")
+ module?.handleError(taskId: task.taskIdentifier, error: error)
+ }
+ }
+
+ func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
+ DispatchQueue.main.async {
+ if let completion = BackgroundDownloaderModule.backgroundCompletionHandler {
+ completion()
+ BackgroundDownloaderModule.backgroundCompletionHandler = nil
+ }
+ }
+ }
+}
+
+public class BackgroundDownloaderModule: Module {
+ private var session: URLSession?
+ private var sessionDelegate: DownloadSessionDelegate?
+ fileprivate static var backgroundCompletionHandler: (() -> Void)?
+ private var downloadTasks: [Int: DownloadTaskInfo] = [:]
+ private var downloadQueue: [(url: String, destinationPath: String?)] = []
+ private var lastProgressTime: [Int: Date] = [:]
+
+ public func definition() -> ModuleDefinition {
+ Name("BackgroundDownloader")
+
+ Events(
+ "onDownloadProgress",
+ "onDownloadComplete",
+ "onDownloadError",
+ "onDownloadStarted"
+ )
+
+ OnCreate {
+ self.initializeSession()
+ }
+
+ AsyncFunction("startDownload") { (urlString: String, destinationPath: String?) -> Int in
+ guard let url = URL(string: urlString) else {
+ throw DownloadError.invalidURL
+ }
+
+ if self.session == nil {
+ self.initializeSession()
+ }
+
+ guard let session = self.session else {
+ throw DownloadError.downloadFailed
+ }
+
+ // Create a URLRequest to ensure proper handling
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+ request.timeoutInterval = 300
+
+ let task = session.downloadTask(with: request)
+ let taskId = task.taskIdentifier
+
+ self.downloadTasks[taskId] = DownloadTaskInfo(
+ url: urlString,
+ destinationPath: destinationPath
+ )
+
+ task.resume()
+
+ self.sendEvent("onDownloadStarted", [
+ "taskId": taskId,
+ "url": urlString
+ ])
+
+ return taskId
+ }
+
+ AsyncFunction("enqueueDownload") { (urlString: String, destinationPath: String?) -> Int in
+ // Add to queue
+ let wasEmpty = self.downloadQueue.isEmpty
+ self.downloadQueue.append((url: urlString, destinationPath: destinationPath))
+
+ // If queue was empty and no active downloads, start processing immediately
+ if wasEmpty {
+ return try await self.processNextInQueue()
+ }
+
+ // Return placeholder taskId for queued items
+ return -1
+ }
+
+ Function("cancelDownload") { (taskId: Int) in
+ self.session?.getAllTasks { tasks in
+ for task in tasks where task.taskIdentifier == taskId {
+ task.cancel()
+ self.downloadTasks.removeValue(forKey: taskId)
+ }
+ }
+ }
+
+ Function("cancelQueuedDownload") { (url: String) in
+ // Remove from queue by URL
+ self.downloadQueue.removeAll { queuedItem in
+ queuedItem.url == url
+ }
+ }
+
+ Function("cancelAllDownloads") {
+ self.session?.getAllTasks { tasks in
+ for task in tasks {
+ task.cancel()
+ }
+ self.downloadTasks.removeAll()
+ }
+ }
+
+ AsyncFunction("getActiveDownloads") { () -> [[String: Any]] in
+ return try await withCheckedThrowingContinuation { continuation in
+ let downloadTasks = self.downloadTasks
+
+ self.session?.getAllTasks { tasks in
+ let activeDownloads = tasks.compactMap { task -> [String: Any]? in
+ guard task is URLSessionDownloadTask,
+ let info = downloadTasks[task.taskIdentifier] else {
+ return nil
+ }
+
+ return [
+ "taskId": task.taskIdentifier,
+ "url": info.url
+ ]
+ }
+ continuation.resume(returning: activeDownloads)
+ }
+ }
+ }
+ }
+
+ private func initializeSession() {
+ print("[BackgroundDownloader] Initializing URLSession")
+
+ let config = URLSessionConfiguration.background(
+ withIdentifier: "com.fredrikburmester.streamyfin.backgrounddownloader"
+ )
+ config.allowsCellularAccess = true
+ config.sessionSendsLaunchEvents = true
+ config.isDiscretionary = false
+
+ self.sessionDelegate = DownloadSessionDelegate(module: self)
+ self.session = URLSession(
+ configuration: config,
+ delegate: self.sessionDelegate,
+ delegateQueue: nil
+ )
+
+ print("[BackgroundDownloader] URLSession initialized with delegate: \(String(describing: self.sessionDelegate))")
+ print("[BackgroundDownloader] Session identifier: \(config.identifier ?? "nil")")
+ print("[BackgroundDownloader] Delegate queue: nil (uses default)")
+
+ // Verify delegate is connected
+ if let session = self.session, session.delegate != nil {
+ print("[BackgroundDownloader] ✅ Delegate successfully attached to session")
+ } else {
+ print("[BackgroundDownloader] ⚠️ DELEGATE NOT ATTACHED!")
+ }
+ }
+
+ // Handler methods called by the delegate
+ func handleProgress(taskId: Int, bytesWritten: Int64, totalBytes: Int64) {
+ let progress = totalBytes > 0
+ ? Double(bytesWritten) / Double(totalBytes)
+ : 0.0
+
+ // Throttle progress updates: only send every 500ms
+ let lastTime = lastProgressTime[taskId] ?? Date.distantPast
+ let now = Date()
+ let timeDiff = now.timeIntervalSince(lastTime)
+
+ // Send if 500ms passed
+ if timeDiff >= 0.5 {
+ self.sendEvent("onDownloadProgress", [
+ "taskId": taskId,
+ "bytesWritten": bytesWritten,
+ "totalBytes": totalBytes,
+ "progress": progress
+ ])
+
+ lastProgressTime[taskId] = now
+ }
+ }
+
+ func handleDownloadComplete(taskId: Int, location: URL, downloadTask: URLSessionDownloadTask) {
+ guard let taskInfo = downloadTasks[taskId] else {
+ self.sendEvent("onDownloadError", [
+ "taskId": taskId,
+ "error": "Download task info not found"
+ ])
+ return
+ }
+
+ let fileManager = FileManager.default
+
+ do {
+ let destinationURL: URL
+
+ if let customPath = taskInfo.destinationPath {
+ destinationURL = URL(fileURLWithPath: customPath)
+ } else {
+ let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let filename = downloadTask.response?.suggestedFilename
+ ?? downloadTask.originalRequest?.url?.lastPathComponent
+ ?? "download_\(taskId)"
+ destinationURL = documentsDir.appendingPathComponent(filename)
+ }
+
+ if fileManager.fileExists(atPath: destinationURL.path) {
+ try fileManager.removeItem(at: destinationURL)
+ }
+
+ let destinationDirectory = destinationURL.deletingLastPathComponent()
+ if !fileManager.fileExists(atPath: destinationDirectory.path) {
+ try fileManager.createDirectory(
+ at: destinationDirectory,
+ withIntermediateDirectories: true,
+ attributes: nil
+ )
+ }
+
+ try fileManager.moveItem(at: location, to: destinationURL)
+
+ self.sendEvent("onDownloadComplete", [
+ "taskId": taskId,
+ "filePath": destinationURL.path,
+ "url": taskInfo.url
+ ])
+
+ downloadTasks.removeValue(forKey: taskId)
+ lastProgressTime.removeValue(forKey: taskId)
+
+ // Process next item in queue
+ Task {
+ do {
+ _ = try await self.processNextInQueue()
+ } catch {
+ print("[BackgroundDownloader] Error processing next: \(error)")
+ }
+ }
+
+ } catch {
+ self.sendEvent("onDownloadError", [
+ "taskId": taskId,
+ "error": "File operation failed: \(error.localizedDescription)"
+ ])
+
+ // Process next item in queue even on error
+ Task {
+ do {
+ _ = try await self.processNextInQueue()
+ } catch {
+ print("[BackgroundDownloader] Error processing next: \(error)")
+ }
+ }
+ }
+ }
+
+ func handleError(taskId: Int, error: Error) {
+ let isCancelled = (error as NSError).code == NSURLErrorCancelled
+
+ if !isCancelled {
+ print("[BackgroundDownloader] Task \(taskId) error: \(error.localizedDescription)")
+
+ self.sendEvent("onDownloadError", [
+ "taskId": taskId,
+ "error": error.localizedDescription
+ ])
+ }
+
+ downloadTasks.removeValue(forKey: taskId)
+ lastProgressTime.removeValue(forKey: taskId)
+
+ // Process next item in queue (whether cancelled or errored)
+ Task {
+ do {
+ _ = try await self.processNextInQueue()
+ } catch {
+ print("[BackgroundDownloader] Error processing next: \(error)")
+ }
+ }
+ }
+
+ private func processNextInQueue() async throws -> Int {
+ // Check if queue has items
+ guard !downloadQueue.isEmpty else {
+ return -1
+ }
+
+ // Check if there are active downloads
+ if !downloadTasks.isEmpty {
+ return -1
+ }
+
+ // Get next item from queue
+ let (url, destinationPath) = downloadQueue.removeFirst()
+ print("[BackgroundDownloader] Starting queued download")
+
+ // Start the download using existing startDownload logic
+ guard let urlObj = URL(string: url) else {
+ print("[BackgroundDownloader] Invalid URL in queue: \(url)")
+ return try await processNextInQueue()
+ }
+
+ if session == nil {
+ initializeSession()
+ }
+
+ guard let session = self.session else {
+ throw DownloadError.downloadFailed
+ }
+
+ var request = URLRequest(url: urlObj)
+ request.httpMethod = "GET"
+ request.timeoutInterval = 300
+
+ let task = session.downloadTask(with: request)
+ let taskId = task.taskIdentifier
+
+ downloadTasks[taskId] = DownloadTaskInfo(
+ url: url,
+ destinationPath: destinationPath
+ )
+
+ task.resume()
+
+ sendEvent("onDownloadStarted", [
+ "taskId": taskId,
+ "url": url
+ ])
+
+ return taskId
+ }
+
+ static func setBackgroundCompletionHandler(_ handler: @escaping () -> Void) {
+ BackgroundDownloaderModule.backgroundCompletionHandler = handler
+ }
+}
+
diff --git a/modules/background-downloader/src/BackgroundDownloader.types.ts b/modules/background-downloader/src/BackgroundDownloader.types.ts
new file mode 100644
index 00000000..566aa5e7
--- /dev/null
+++ b/modules/background-downloader/src/BackgroundDownloader.types.ts
@@ -0,0 +1,41 @@
+import type { EventSubscription } from "expo-modules-core";
+
+export interface DownloadProgressEvent {
+ taskId: number;
+ bytesWritten: number;
+ totalBytes: number;
+ progress: number;
+}
+
+export interface DownloadCompleteEvent {
+ taskId: number;
+ filePath: string;
+ url: string;
+}
+
+export interface DownloadErrorEvent {
+ taskId: number;
+ error: string;
+}
+
+export interface DownloadStartedEvent {
+ taskId: number;
+ url: string;
+}
+
+export interface ActiveDownload {
+ taskId: number;
+ url: string;
+ state: "running" | "suspended" | "canceling" | "completed" | "unknown";
+}
+
+export interface BackgroundDownloaderModuleType {
+ startDownload(url: string, destinationPath?: string): Promise;
+ cancelDownload(taskId: number): void;
+ cancelAllDownloads(): void;
+ getActiveDownloads(): Promise;
+ addListener(
+ eventName: string,
+ listener: (event: any) => void,
+ ): EventSubscription;
+}
diff --git a/modules/background-downloader/src/BackgroundDownloaderModule.ts b/modules/background-downloader/src/BackgroundDownloaderModule.ts
new file mode 100644
index 00000000..d2df92b8
--- /dev/null
+++ b/modules/background-downloader/src/BackgroundDownloaderModule.ts
@@ -0,0 +1,7 @@
+import { requireNativeModule } from "expo-modules-core";
+import type { BackgroundDownloaderModuleType } from "./BackgroundDownloader.types";
+
+const BackgroundDownloaderModule: BackgroundDownloaderModuleType =
+ requireNativeModule("BackgroundDownloader");
+
+export default BackgroundDownloaderModule;
diff --git a/modules/index.ts b/modules/index.ts
index aa4b7647..d0ea5cd2 100644
--- a/modules/index.ts
+++ b/modules/index.ts
@@ -12,6 +12,16 @@ import type {
} from "./VlcPlayer.types";
import VlcPlayerView from "./VlcPlayerView";
+export type {
+ ActiveDownload,
+ DownloadCompleteEvent,
+ DownloadErrorEvent,
+ DownloadProgressEvent,
+ DownloadStartedEvent,
+} from "./background-downloader";
+// Background Downloader
+export { default as BackgroundDownloader } from "./background-downloader";
+
// Component
export { VlcPlayerView };
diff --git a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt
index c4517802..e164efb9 100644
--- a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt
+++ b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt
@@ -212,9 +212,7 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
fun setSource(source: Map) {
log.debug("setting source $source")
if (hasSource) {
- log.debug("Source already set. Resuming")
- mediaPlayer?.attachViews(videoLayout, null, false, false)
- play()
+ log.debug("Source already set. Ignoring.")
return
}
val mediaOptions = source["mediaOptions"] as? Map ?: emptyMap()
diff --git a/package.json b/package.json
index b021f8b7..c0f711f7 100644
--- a/package.json
+++ b/package.json
@@ -22,120 +22,118 @@
"test": "bun run typecheck && bun run lint && bun run format && bun run doctor"
},
"dependencies": {
- "@bottom-tabs/react-navigation": "^0.12.0",
- "@expo/metro-runtime": "~5.0.5",
+ "@bottom-tabs/react-navigation": "^1.0.2",
+ "@expo/metro-runtime": "~6.1.1",
"@expo/react-native-action-sheet": "^4.1.1",
- "@expo/vector-icons": "^14.1.0",
+ "@expo/ui": "^0.2.0-beta.4",
+ "@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "^5.1.0",
- "@jellyfin/sdk": "^0.11.0",
- "@kesha-antonov/react-native-background-downloader": "^3.2.6",
+ "@jellyfin/sdk": "^0.13.0",
"@react-native-community/netinfo": "^11.4.1",
- "@react-native-menu/menu": "1.2.3",
"@react-navigation/material-top-tabs": "^7.2.14",
"@react-navigation/native": "^7.0.14",
- "@shopify/flash-list": "^1.8.3",
+ "@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
- "expo": "^53.0.23",
- "expo-application": "~6.1.4",
- "expo-asset": "~11.1.7",
- "expo-background-task": "~0.2.8",
- "expo-blur": "~14.1.4",
- "expo-brightness": "~13.1.4",
- "expo-build-properties": "~0.14.6",
- "expo-constants": "~17.1.5",
- "expo-device": "~7.1.4",
- "expo-font": "~13.3.1",
- "expo-haptics": "~14.1.4",
- "expo-image": "~2.4.0",
- "expo-linear-gradient": "~14.1.4",
- "expo-linking": "~7.1.4",
- "expo-localization": "~16.1.5",
- "expo-notifications": "~0.31.2",
- "expo-router": "~5.1.7",
- "expo-screen-orientation": "~8.1.6",
- "expo-sensors": "~14.1.4",
- "expo-sharing": "~13.1.5",
- "expo-splash-screen": "~0.30.8",
- "expo-status-bar": "~2.2.3",
- "expo-system-ui": "~5.0.11",
- "expo-task-manager": "~13.1.6",
- "expo-web-browser": "~14.2.0",
+ "expo": "^54.0.23",
+ "expo-application": "~7.0.5",
+ "expo-asset": "~12.0.6",
+ "expo-background-task": "~1.0.5",
+ "expo-blur": "~15.0.5",
+ "expo-brightness": "~14.0.5",
+ "expo-build-properties": "~1.0.6",
+ "expo-constants": "~18.0.10",
+ "expo-dev-client": "~6.0.17",
+ "expo-device": "~8.0.5",
+ "expo-font": "~14.0.9",
+ "expo-haptics": "~15.0.5",
+ "expo-image": "~3.0.10",
+ "expo-linear-gradient": "~15.0.5",
+ "expo-linking": "~8.0.6",
+ "expo-localization": "~17.0.5",
+ "expo-notifications": "~0.32.7",
+ "expo-router": "~6.0.14",
+ "expo-screen-orientation": "~9.0.5",
+ "expo-sensors": "~15.0.5",
+ "expo-sharing": "~14.0.5",
+ "expo-splash-screen": "~31.0.7",
+ "expo-status-bar": "~3.0.6",
+ "expo-system-ui": "~6.0.8",
+ "expo-task-manager": "~14.0.8",
+ "expo-web-browser": "~15.0.9",
"i18next": "^25.0.0",
"jotai": "^2.12.5",
"lodash": "^4.17.21",
"nativewind": "^2.0.11",
- "react": "19.0.0",
- "react-dom": "19.0.0",
+ "patch-package": "^8.0.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
"react-i18next": "^15.4.0",
- "react-native": "npm:react-native-tvos@0.79.5-0",
+ "react-native": "npm:react-native-tvos@0.81.5-1",
"react-native-awesome-slider": "^2.9.0",
- "react-native-bottom-tabs": "^0.12.0",
+ "react-native-bottom-tabs": "^1.0.2",
"react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2",
"react-native-country-flag": "^2.0.2",
"react-native-device-info": "^14.0.4",
- "react-native-gesture-handler": "~2.24.0",
- "react-native-google-cast": "^4.9.0",
+ "react-native-edge-to-edge": "^1.7.0",
+ "react-native-gesture-handler": "~2.28.0",
+ "react-native-google-cast": "^4.9.1",
"react-native-image-colors": "^2.4.0",
- "react-native-ios-context-menu": "^3.1.0",
- "react-native-ios-utilities": "5.1.8",
- "react-native-mmkv": "2.12.2",
+ "react-native-ios-context-menu": "^3.2.1",
+ "react-native-ios-utilities": "5.2.0",
+ "react-native-mmkv": "4.0.0",
+ "react-native-nitro-modules": "^0.31.5",
"react-native-pager-view": "^6.9.1",
- "react-native-reanimated": "~3.19.1",
+ "react-native-reanimated": "~4.1.1",
"react-native-reanimated-carousel": "4.0.2",
- "react-native-safe-area-context": "5.4.0",
- "react-native-screens": "~4.11.1",
- "react-native-svg": "15.11.2",
+ "react-native-safe-area-context": "~5.6.0",
+ "react-native-screens": "~4.18.0",
+ "react-native-svg": "15.12.1",
"react-native-udp": "^4.1.7",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
- "react-native-video": "6.14.1",
+ "react-native-video": "6.16.1",
"react-native-volume-manager": "^2.0.8",
- "react-native-web": "^0.20.0",
+ "react-native-web": "^0.21.0",
+ "react-native-worklets": "0.5.1",
"sonner-native": "^0.21.0",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
- "zeego": "^3.0.6",
"zod": "^4.1.3"
},
"devDependencies": {
- "@babel/core": "7.28.5",
- "@biomejs/biome": "2.2.7",
- "@react-native-community/cli": "20.0.2",
- "@react-native-tvos/config-tv": "0.1.4",
- "@types/jest": "30.0.0",
- "@types/lodash": "4.17.20",
- "@types/react": "~19.0.10",
- "@types/react-test-renderer": "19.1.0",
- "cross-env": "10.1.0",
- "expo-dev-client": "5.2.4",
- "expo-doctor": "1.17.11",
- "husky": "9.1.7",
- "lint-staged": "16.2.6",
+ "@babel/core": "^7.20.0",
+ "@biomejs/biome": "^2.2.4",
+ "@react-native-community/cli": "^20.0.0",
+ "@react-native-tvos/config-tv": "^0.1.1",
+ "@types/jest": "^29.5.12",
+ "@types/lodash": "^4.17.15",
+ "@types/react": "~19.1.10",
+ "@types/react-test-renderer": "^19.0.0",
+ "cross-env": "^10.0.0",
+ "expo-doctor": "^1.17.0",
+ "husky": "^9.1.7",
+ "lint-staged": "^16.1.5",
+ "postinstall-postinstall": "^2.1.0",
"react-test-renderer": "19.1.1",
- "typescript": "5.8.3"
+ "typescript": "~5.9.2"
},
"expo": {
- "install": {
- "exclude": [
- "react-native",
- "@shopify/flash-list",
- "react-native-reanimated",
- "react-native-pager-view"
- ]
- },
"doctor": {
"reactNativeDirectoryCheck": {
"exclude": [
"react-native-google-cast",
"react-native-udp",
- "@bottom-tabs/react-navigation",
- "@jellyfin/sdk",
- "expo-doctor"
+ "@jellyfin/sdk"
],
"listUnknownPackages": false
}
+ },
+ "install": {
+ "exclude": [
+ "react-native-screens"
+ ]
}
},
"private": true,
@@ -149,5 +147,9 @@
},
"trustedDependencies": [
"unrs-resolver"
- ]
+ ],
+ "resolutions": {
+ "expo-constants": "~18.0.10",
+ "expo-task-manager": "~14.0.8"
+ }
}
diff --git a/patches/@react-native-menu+menu+1.2.4.patch b/patches/@react-native-menu+menu+1.2.4.patch
new file mode 100644
index 00000000..be8e245d
--- /dev/null
+++ b/patches/@react-native-menu+menu+1.2.4.patch
@@ -0,0 +1,58 @@
+diff --git a/node_modules/@react-native-menu/menu/android/.DS_Store b/node_modules/@react-native-menu/menu/android/.DS_Store
+new file mode 100644
+index 0000000..5008ddf
+Binary files /dev/null and b/node_modules/@react-native-menu/menu/android/.DS_Store differ
+diff --git a/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuView.kt b/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuView.kt
+index 17ed7c6..c45f5cc 100644
+--- a/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuView.kt
++++ b/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuView.kt
+@@ -24,6 +24,11 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) {
+ private var mIsOnLongPress = false
+ private var mGestureDetector: GestureDetector
+ private var mHitSlopRect: Rect? = null
++ set(value) {
++ super.hitSlopRect = value
++ mHitSlopRect = value
++ updateTouchDelegate()
++ }
+
+ init {
+ mGestureDetector = GestureDetector(mContext, object : GestureDetector.SimpleOnGestureListener() {
+@@ -47,12 +52,6 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) {
+ prepareMenu()
+ }
+
+- override fun setHitSlopRect(rect: Rect?) {
+- super.setHitSlopRect(rect)
+- mHitSlopRect = rect
+- updateTouchDelegate()
+- }
+-
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+ return true
+ }
+diff --git a/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt b/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt
+index 4731e1a..e4d2743 100644
+--- a/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt
++++ b/node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt
+@@ -123,9 +123,9 @@ abstract class MenuViewManagerBase : ReactClippingViewManager() {
+ fun setHitSlop(view: ReactViewGroup, @Nullable hitSlop: ReadableMap?) {
+ if (hitSlop == null) {
+ // We should keep using setters as `Val cannot be reassigned`
+- view.setHitSlopRect(null)
++ view.hitSlopRect = null
+ } else {
+- view.setHitSlopRect(
++ view.hitSlopRect = (
+ Rect(
+ if (hitSlop.hasKey("left"))
+ PixelUtil.toPixelFromDIP(hitSlop.getDouble("left")).toInt()
+@@ -206,7 +206,7 @@ abstract class MenuViewManagerBase : ReactClippingViewManager() {
+
+ @ReactProp(name = ViewProps.OVERFLOW)
+ fun setOverflow(view: ReactViewGroup, overflow: String?) {
+- view.setOverflow(overflow)
++ view.overflow = overflow
+ }
+
+ @ReactProp(name = "backfaceVisibility")
diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx
index ffb15e9e..4610c30b 100644
--- a/providers/DownloadProvider.tsx
+++ b/providers/DownloadProvider.tsx
@@ -1,325 +1,55 @@
-import type {
- BaseItemDto,
- MediaSourceInfo,
-} from "@jellyfin/sdk/lib/generated-client/models";
import * as Application from "expo-application";
-import * as FileSystem from "expo-file-system";
-import * as Notifications from "expo-notifications";
-import { router } from "expo-router";
+import { Directory, Paths } from "expo-file-system";
import { atom, useAtom } from "jotai";
-import { throttle } from "lodash";
-import React, {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
-} from "react";
-import { useTranslation } from "react-i18next";
+import { createContext, useCallback, useContext, useMemo, useRef } from "react";
import { Platform } from "react-native";
-import { toast } from "sonner-native";
import { useHaptic } from "@/hooks/useHaptic";
-import useImageStorage from "@/hooks/useImageStorage";
-import { useInterval } from "@/hooks/useInterval";
-import { useSettings } from "@/utils/atoms/settings";
-import { getOrSetDeviceId } from "@/utils/device";
-import useDownloadHelper from "@/utils/download";
-import { getItemImage } from "@/utils/getItemImage";
-import { dumpDownloadDiagnostics, writeToLog } from "@/utils/log";
-import { storage } from "@/utils/mmkv";
-import { fetchAndParseSegments } from "@/utils/segments";
-import { generateTrickplayUrl, getTrickplayInfo } from "@/utils/trickplay";
-import { Bitrate } from "../components/BitrateSelector";
import {
- DownloadedItem,
- DownloadsDatabase,
- JobStatus,
- TrickPlayData,
-} from "./Downloads/types";
+ getAllDownloadedItems,
+ getDownloadedItemById,
+ getDownloadsDatabase,
+} from "./Downloads/database";
+import { getDownloadedItemSize } from "./Downloads/fileOperations";
+import { useDownloadEventHandlers } from "./Downloads/hooks/useDownloadEventHandlers";
+import { useDownloadOperations } from "./Downloads/hooks/useDownloadOperations";
+import type { JobStatus } from "./Downloads/types";
import { apiAtom } from "./JellyfinProvider";
-const BackGroundDownloader = !Platform.isTV
- ? require("@kesha-antonov/react-native-background-downloader")
- : null;
-
-// Cap progress at 99% to avoid showing 100% before the download is actually complete
-const MAX_PROGRESS_BEFORE_COMPLETION = 99;
-
-// Estimate the total download size in bytes for a job. If the media source
-// provides a Size, use that. Otherwise, if we have a bitrate and run time
-// (RunTimeTicks), approximate size = (bitrate bits/sec * seconds) / 8.
-const calculateEstimatedSize = (p: JobStatus): number => {
- const size = p.mediaSource?.Size || 0;
- const maxBitrate = p.maxBitrate?.value;
- const runTimeTicks = (p.item?.RunTimeTicks || 0) as number;
-
- if (!size && maxBitrate && runTimeTicks > 0) {
- // Jellyfin RunTimeTicks are in 10,000,000 ticks per second
- const seconds = runTimeTicks / 10000000;
- if (seconds > 0) {
- // maxBitrate is in bits per second; convert to bytes
- return Math.round((maxBitrate / 8) * seconds);
- }
- }
-
- return size || 0;
-};
-
-// Calculate download speed in bytes/sec based on a job's last update time
-// and previously recorded bytesDownloaded.
-const calculateSpeed = (
- p: JobStatus,
- currentBytesDownloaded?: number,
-): number | undefined => {
- // Prefer session-only deltas when available: lastSessionBytes + lastSessionUpdateTime
- const now = Date.now();
-
- if (p.lastSessionUpdateTime && p.lastSessionBytes !== undefined) {
- const last = new Date(p.lastSessionUpdateTime).getTime();
- const deltaTime = (now - last) / 1000;
- if (deltaTime > 0) {
- const current =
- currentBytesDownloaded ?? p.bytesDownloaded ?? p.lastSessionBytes;
- const deltaBytes = current - p.lastSessionBytes;
- if (deltaBytes > 0) return deltaBytes / deltaTime;
- }
- }
-
- // Fallback to total-based deltas for compatibility
- if (!p.lastProgressUpdateTime || p.bytesDownloaded === undefined)
- return undefined;
- const last = new Date(p.lastProgressUpdateTime).getTime();
- const deltaTime = (now - last) / 1000;
- if (deltaTime <= 0) return undefined;
- const prev = p.bytesDownloaded || 0;
- const current = currentBytesDownloaded ?? prev;
- const deltaBytes = current - prev;
- if (deltaBytes <= 0) return undefined;
- return deltaBytes / deltaTime;
-};
-
export const processesAtom = atom([]);
-const DOWNLOADS_DATABASE_KEY = "downloads.v2.json";
+export const downloadsRefreshAtom = atom(0);
const DownloadContext = createContext | null>(null);
function useDownloadProvider() {
- const { t } = useTranslation();
const [api] = useAtom(apiAtom);
- const { saveSeriesPrimaryImage } = useDownloadHelper();
- const { saveImage } = useImageStorage();
const [processes, setProcesses] = useAtom(processesAtom);
- const { settings } = useSettings();
+ const [refreshKey, setRefreshKey] = useAtom(downloadsRefreshAtom);
const successHapticFeedback = useHaptic("success");
- // Generate notification content based on item type
- const getNotificationContent = useCallback(
- (item: BaseItemDto, isSuccess: boolean) => {
- if (item.Type === "Episode") {
- const season = item.ParentIndexNumber
- ? String(item.ParentIndexNumber).padStart(2, "0")
- : "??";
- const episode = item.IndexNumber
- ? String(item.IndexNumber).padStart(2, "0")
- : "??";
- const subtitle = `${item.Name} - [S${season}E${episode}] (${item.SeriesName})`;
+ // Track task ID to process ID mapping
+ const taskMapRef = useRef