From a68d8500a6e606306c6d2fb0556740feafca8747 Mon Sep 17 00:00:00 2001 From: Gauvain <68083474+Gauvino@users.noreply.github.com> Date: Fri, 29 Aug 2025 22:06:50 +0200 Subject: [PATCH] chore: update dependencies and refactor config plugin imports (#993) --- .github/workflows/linting.yml | 10 +- app.config.js | 2 + app/(auth)/(tabs)/(home)/downloads/index.tsx | 2 +- .../livetv/guide.tsx | 2 +- .../series/[id].tsx | 2 +- app/(auth)/player/direct-player.tsx | 24 +++- app/_layout.tsx | 61 +++++---- bun.lock | 99 ++++++++------ components/DownloadItem.tsx | 26 +++- components/ItemContent.tsx | 2 +- components/PlayButton.tsx | 30 +++- components/downloads/ActiveDownloads.tsx | 91 ++++++++----- components/downloads/MovieCard.tsx | 2 +- components/downloads/SeriesCard.tsx | 2 +- components/home/Favorites.tsx | 6 +- components/home/LargeMovieCarousel.tsx | 2 +- components/settings/OtherSettings.tsx | 12 +- components/video-player/controls/Controls.tsx | 12 +- .../video-player/controls/TrickplayBubble.tsx | 1 - hooks/useCreditSkipper.ts | 11 +- hooks/useIntroSkipper.ts | 11 +- hooks/useJellyseerr.ts | 10 +- hooks/useTrickplay.ts | 93 ++++--------- package.json | 15 +- plugins/withAndroidManifest.js | 17 ++- plugins/withChangeNativeAndroidTextToWhite.js | 2 +- plugins/withRNBackgroundDownloader.js | 4 +- plugins/withTrustLocalCerts.js | 22 +-- providers/DownloadProvider.tsx | 72 +++++++++- providers/JellyfinProvider.tsx | 7 +- translations/en.json | 8 +- utils/atoms/settings.ts | 11 +- utils/background-tasks.ts | 128 +++++++++++++----- utils/segments.ts | 29 ++-- utils/trickplay.ts | 65 +++++++++ 35 files changed, 591 insertions(+), 302 deletions(-) create mode 100644 utils/trickplay.ts diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index b5db41fd..90b4af30 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -53,7 +53,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 - name: Dependency Review @@ -71,7 +71,7 @@ jobs: - name: 🛒 Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive fetch-depth: 0 @@ -88,7 +88,7 @@ jobs: code_quality: name: "🔍 Lint & Test (${{ matrix.command }})" - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -96,11 +96,13 @@ jobs: - "lint" - "check" - "format" + - "typecheck" + steps: - name: "📥 Checkout PR code" uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive fetch-depth: 0 diff --git a/app.config.js b/app.config.js index 43c7c073..6621e022 100644 --- a/app.config.js +++ b/app.config.js @@ -1,5 +1,7 @@ module.exports = ({ config }) => { if (process.env.EXPO_TV !== "1") { + config.plugins.push("expo-background-task"); + config.plugins.push([ "react-native-google-cast", { useDefaultExpandedMediaControls: true }, diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index ae5c32f5..1bd33ba2 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -14,7 +14,7 @@ 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"; +import ActiveDownloads from "@/components/downloads/ActiveDownloads"; import { DownloadSize } from "@/components/downloads/DownloadSize"; import { MovieCard } from "@/components/downloads/MovieCard"; import { SeriesCard } from "@/components/downloads/SeriesCard"; diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx index 6fa6d25e..390e8eb6 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/livetv/guide.tsx @@ -116,8 +116,8 @@ export default function page() { style={{ width: "100%", height: "100%", - resizeMode: "contain", }} + contentFit='contain' item={c} /> 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 bf22d464..78cad4a3 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx @@ -141,8 +141,8 @@ const page: React.FC = () => { style={{ height: 130, width: "100%", - resizeMode: "contain", }} + contentFit='contain' /> ) : undefined } diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 17a06798..71784aa7 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -68,6 +68,7 @@ export default function page() { : require("react-native-volume-manager"); const downloadUtils = useDownload(); + const downloadedFiles = downloadUtils.getDownloadedItems(); const revalidateProgressCache = useInvalidatePlaybackProgressCache(); @@ -175,6 +176,13 @@ export default function page() { const fetchStreamData = async () => { setStreamStatus({ isLoading: true, isError: false }); try { + // Don't attempt to fetch stream data if item is not available + if (!item?.Id) { + console.log("Item not loaded yet, skipping stream data fetch"); + setStreamStatus({ isLoading: false, isError: false }); + return; + } + let result: Stream | null = null; if (offline && downloadedItem && downloadedItem.mediaSource) { const url = downloadedItem.videoFilePath; @@ -186,13 +194,25 @@ export default function page() { }; } } else { + // Validate required parameters before calling getStreamUrl + if (!api) { + console.warn("API not available for streaming"); + setStreamStatus({ isLoading: false, isError: true }); + return; + } + if (!user?.Id) { + console.warn("User not authenticated for streaming"); + setStreamStatus({ isLoading: false, isError: true }); + return; + } + const native = generateDeviceProfile(); const transcoding = generateDeviceProfile({ transcode: true }); const res = await getStreamUrl({ api, item, startTimeTicks: getInitialPlaybackTicks(), - userId: user?.Id, + userId: user.Id, audioStreamIndex: audioIndex, maxStreamingBitrate: bitrateValue, mediaSourceId: mediaSourceId, @@ -728,6 +748,8 @@ export default function page() { setAspectRatio={setAspectRatio} setScaleFactor={setScaleFactor} isVlc + api={api} + downloadedFiles={downloadedFiles} /> )} diff --git a/app/_layout.tsx b/app/_layout.tsx index dcc0583b..be251c4e 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -33,28 +33,25 @@ const BackGroundDownloader = !Platform.isTV import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -const BackgroundFetch = !Platform.isTV - ? require("expo-background-fetch") - : null; +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 ScreenOrientation from "@/packages/expo-screen-orientation"; -const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; - -import { getLocales } from "expo-localization"; +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 type { EventSubscription } from "expo-modules-core"; @@ -136,7 +133,7 @@ if (!Platform.isTV) { const result = response.data.filter((s) => s.NowPlayingItem); Notifications.setBadgeCountAsync(result.length); - return BackgroundFetch.BackgroundFetchResult.NewData; + return BackgroundTask.BackgroundTaskResult.Success; }); TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { @@ -144,22 +141,22 @@ if (!Platform.isTV) { const settingsData = storage.getString("settings"); - if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData; + if (!settingsData) return BackgroundTask.BackgroundTaskResult.Failed; const settings: Partial = JSON.parse(settingsData); if (!settings?.autoDownload) - return BackgroundFetch.BackgroundFetchResult.NoData; + return BackgroundTask.BackgroundTaskResult.Failed; const token = getTokenFromStorage(); const deviceId = getOrSetDeviceId(); const baseDirectory = FileSystem.documentDirectory; if (!token || !deviceId || !baseDirectory) - return BackgroundFetch.BackgroundFetchResult.NoData; + return BackgroundTask.BackgroundTaskResult.Failed; // Be sure to return the successful result type! - return BackgroundFetch.BackgroundFetchResult.NewData; + return BackgroundTask.BackgroundTaskResult.Success; }); } @@ -168,22 +165,31 @@ const checkAndRequestPermissions = async () => { const hasAskedBefore = storage.getString( "hasAskedForNotificationPermission", ); - + let granted = false; if (hasAskedBefore !== "true") { const { status } = await Notifications.requestPermissionsAsync(); - - if (status === "granted") { + granted = status === "granted"; + if (granted) { writeToLog("INFO", "Notification permissions granted."); console.log("Notification permissions granted."); } else { writeToLog("ERROR", "Notification permissions denied."); console.log("Notification permissions denied."); } - storage.set("hasAskedForNotificationPermission", "true"); } else { - console.log("Already asked for notification permissions before."); + // Already asked before, check current status + const { status } = await Notifications.getPermissionsAsync(); + granted = status === "granted"; + if (!granted) { + writeToLog( + "ERROR", + "Notification permissions denied (already asked before).", + ); + console.log("Notification permissions denied (already asked before)."); + } } + return granted; } catch (error) { writeToLog( "ERROR", @@ -191,6 +197,7 @@ const checkAndRequestPermissions = async () => { error, ); console.error("Error checking/requesting notification permissions:", error); + return false; } }; @@ -264,7 +271,13 @@ function Layout() { }); } - await checkAndRequestPermissions(); + const granted = await checkAndRequestPermissions(); + if (!granted) { + console.log( + "Notification permissions not granted, skipping background fetch and push token registration.", + ); + return; + } if (!Platform.isTV && user && user.Policy?.IsAdministrator) { await registerBackgroundFetchAsyncSessions(); @@ -280,7 +293,7 @@ function Layout() { useEffect(() => { if (!Platform.isTV) { - registerNotifications(); + void registerNotifications(); notificationListener.current = Notifications?.addNotificationReceivedListener( @@ -332,14 +345,8 @@ function Layout() { ); return () => { - notificationListener.current && - Notifications?.removeNotificationSubscription( - notificationListener.current, - ); - responseListener.current && - Notifications?.removeNotificationSubscription( - responseListener.current, - ); + notificationListener.current?.remove(); + responseListener.current?.remove(); }; } }, [user, api]); diff --git a/bun.lock b/bun.lock index 9db59340..912b358c 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "name": "streamyfin", "dependencies": { "@bottom-tabs/react-navigation": "^0.9.2", - "@expo/config-plugins": "~10.1.1", "@expo/metro-runtime": "~5.0.4", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^14.1.0", @@ -19,10 +18,10 @@ "@shopify/flash-list": "^1.8.3", "@tanstack/react-query": "^5.66.0", "axios": "^1.7.9", - "expo": "^53.0.6", + "expo": "^53.0.22", "expo-application": "~6.1.4", "expo-asset": "~11.1.7", - "expo-background-fetch": "~13.1.5", + "expo-background-task": "~0.2.8", "expo-blur": "~14.1.4", "expo-brightness": "~13.1.4", "expo-build-properties": "~0.14.6", @@ -36,14 +35,14 @@ "expo-linking": "~7.1.4", "expo-localization": "~16.1.5", "expo-notifications": "~0.31.2", - "expo-router": "~5.1.4", + "expo-router": "~5.1.5", "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.7", - "expo-task-manager": "~13.1.5", + "expo-system-ui": "~5.0.11", + "expo-task-manager": "~13.1.6", "expo-web-browser": "~14.2.0", "i18next": "^25.0.0", "jotai": "^2.12.5", @@ -66,7 +65,7 @@ "react-native-ios-utilities": "5.1.8", "react-native-mmkv": "2.12.2", "react-native-pager-view": "^6.9.1", - "react-native-reanimated": "~3.16.7", + "react-native-reanimated": "~3.17.4", "react-native-reanimated-carousel": "4.0.2", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", @@ -324,7 +323,7 @@ "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], - "@expo/cli": ["@expo/cli@0.24.20", "", { "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.11", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.79.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", "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-uF1pOVcd+xizNtVTuZqNGzy7I6IJon5YMmQidsURds1Ww96AFDxrR/NEACqeATNAmY60m8wy1VZZpSg5zLNkpw=="], + "@expo/cli": ["@expo/cli@0.24.21", "", { "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.11", "@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-DT6K9vgFHqqWL/19mU1ofRcPoO1pn4qmgi76GtuiNU4tbBe/02mRHwFsQw7qRfFAT28If5e/wiwVozgSuZVL8g=="], "@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=="], @@ -358,6 +357,8 @@ "@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=="], + "@expo/schema-utils": ["@expo/schema-utils@0.1.0", "", {}, "sha512-Me2avOfbcVT/O5iRmPKLCCSvbCfVfxIstGMlzVJOffplaZX1+ut8D18siR1wx5fkLMTWKs14ozEz11cGUY7hcw=="], + "@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=="], @@ -544,23 +545,23 @@ "@react-native/assets-registry": ["@react-native/assets-registry@0.79.5", "", {}, "sha512-N4Kt1cKxO5zgM/BLiyzuuDNquZPiIgfktEQ6TqJ/4nKA8zr4e8KJgU6Tb2eleihDO4E24HmkvGc73naybKRz/w=="], - "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.79.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.79.5" } }, "sha512-Rt/imdfqXihD/sn0xnV4flxxb1aLLjPtMF1QleQjEhJsTUPpH4TFlfOpoCvsrXoDl4OIcB1k4FVM24Ez92zf5w=="], + "@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-preset": ["@react-native/babel-preset@0.79.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.79.5", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-GDUYIWslMLbdJHEgKNfrOzXk8EDKxKzbwmBXUugoiSlr6TyepVZsj3GZDLEFarOcTwH1EXXHJsixihk8DCRQDA=="], + "@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/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/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/debugger-frontend": ["@react-native/debugger-frontend@0.79.5", "", {}, "sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A=="], + "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.79.6", "", {}, "sha512-lIK/KkaH7ueM22bLO0YNaQwZbT/oeqhaghOvmZacaNVbJR1Cdh/XAqjT8FgCS+7PUnbxA8B55NYNKGZG3O2pYw=="], - "@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/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/gradle-plugin": ["@react-native/gradle-plugin@0.79.5", "", {}, "sha512-K3QhfFNKiWKF3HsCZCEoWwJPSMcPJQaeqOmzFP4RL8L3nkpgUwn74PfSCcKHxooVpS6bMvJFQOz7ggUZtNVT+A=="], "@react-native/js-polyfills": ["@react-native/js-polyfills@0.79.5", "", {}, "sha512-a2wsFlIhvd9ZqCD5KPRsbCQmbZi6KxhRN++jrqG0FUTEV5vY7MvjjUqDILwJd2ZBZsf7uiDuClCcKqA+EEdbvw=="], - "@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.5", "", {}, "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ=="], + "@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.6", "", {}, "sha512-0v2/ruY7eeKun4BeKu+GcfO+SHBdl0LJn4ZFzTzjHdWES0Cn+ONqKljYaIv8p9MV2Hx/kcdEvbY4lWI34jC/mQ=="], "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.4.6", "", { "dependencies": { "@react-navigation/elements": "^2.6.3", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-f4khxwcL70O5aKfZFbxyBo5RnzPFnBNSXmrrT7q9CRmvN4mHov9KFKGQ3H4xD5sLonsTBtyjvyvPfyEC4G7f+g=="], @@ -618,8 +619,6 @@ "@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], "@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], @@ -674,10 +673,6 @@ "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], - "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], @@ -736,7 +731,7 @@ "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.3", "", { "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.5", "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-wQJn92lqj8GKR7Ojg/aW4+GkqI6ZdDNTDyOqhhl7A9bAqk6t0ukUOWLDXQb4p0qKJjMDV1F6gNWasI2KUbuVTQ=="], + "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-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=="], @@ -988,13 +983,13 @@ "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.20", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.24.20", "@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.3", "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-Nh+HIywVy9KxT/LtH08QcXqrxtUOA9BZhsXn3KCsAYA+kNb80M8VKN8/jfQF+I6CgeKyFKJoPNsWgI0y0VBGrA=="], + "expo": ["expo@53.0.22", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.24.21", "@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-sJ2I4W/e5iiM4u/wYCe3qmW4D7WPCRqByPDD0hJcdYNdjc9HFFFdO4OAudZVyC/MmtoWZEIH5kTJP1cw9FjzYA=="], "expo-application": ["expo-application@6.1.5", "", { "peerDependencies": { "expo": "*" } }, "sha512-ToImFmzw8luY043pWFJhh2ZMm4IwxXoHXxNoGdlhD4Ym6+CCmkAvCglg0FK8dMLzAb+/XabmOE7Rbm8KZb6NZg=="], "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-background-fetch": ["expo-background-fetch@13.1.6", "", { "dependencies": { "expo-task-manager": "~13.1.6" }, "peerDependencies": { "expo": "*" } }, "sha512-hl4kR32DaxoHFYqNsILLZG2mWssCkUb4wnEAHtDGmpxUP4SCnJILcAn99J6AGDFUw5lF6FXNZZCXNfcrFioO4Q=="], + "expo-background-task": ["expo-background-task@0.2.8", "", { "dependencies": { "expo-task-manager": "~13.1.6" }, "peerDependencies": { "expo": "*" } }, "sha512-dePyskpmyDZeOtbr9vWFh+Nrse0TvF6YitJqnKcd+3P7pDMiDr1V2aT6zHdNOc5iV9vPaDJoH/zdmlarp1uHMQ=="], "expo-blur": ["expo-blur@14.1.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-CCLJHxN4eoAl06ESKT3CbMasJ98WsjF9ZQEJnuxtDb9ffrYbZ+g9ru84fukjNUOTtc8A8yXE5z8NgY1l0OMrmQ=="], @@ -1042,7 +1037,7 @@ "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-router": ["expo-router@5.1.4", "", { "dependencies": { "@expo/metro-runtime": "5.0.4", "@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", "schema-utils": "^4.0.1", "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-8GulCelVN9x+VSOio74K1ZYTG6VyCdJw417gV+M/J8xJOZZTA7rFxAdzujBZZ7jd6aIAG7WEwOUU3oSvUO76Vw=="], + "expo-router": ["expo-router@5.1.5", "", { "dependencies": { "@expo/metro-runtime": "5.0.4", "@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-VPhS21DPP+riJIUshs/qpb11L/nzmRK7N7mqSFCr/mjpziznYu/qS+BPeQ88akIuXv6QsXipY5UTfYINdV+P0Q=="], "expo-screen-orientation": ["expo-screen-orientation@8.1.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-nYwadYtdU6mMDk0MCHMPPPQtBoeFYJ2FspLRW+J35CMLqzE4nbpwGeiImfXzkvD94fpOCfI4KgLj5vGauC3pfA=="], @@ -1054,7 +1049,7 @@ "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-system-ui": ["expo-system-ui@5.0.10", "", { "dependencies": { "@react-native/normalize-colors": "0.79.5", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-BTXbSyJr80yuN6VO4XQKZj7BjesZQLHgOYZ0bWyf4VB19GFZq7ZnZOEc/eoKk1B3eIocOMKUfNCrg/Wn8Kfcuw=="], + "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-task-manager": ["expo-task-manager@13.1.6", "", { "dependencies": { "unimodules-app-loader": "~5.1.3" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-sYNAftpIeZ+j6ur17Jo0OpSTk9ks/MDvTbrNCimXMyjIt69XXYL/kAPYf76bWuxOuN8bcJ8Ef8YvihkwFG9hDA=="], @@ -1138,7 +1133,7 @@ "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], - "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=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -1642,7 +1637,7 @@ "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.16.7", "", { "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" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-qoUUQOwE1pHlmQ9cXTJ2MX9FQ9eHllopCLiWOkDkp6CER95ZWeXhJCP4cSm6AD4jigL5jHcZf/SkWrg8ttZUsw=="], + "react-native-reanimated": ["react-native-reanimated@3.17.5", "", { "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-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw=="], "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=="], @@ -1736,8 +1731,6 @@ "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], @@ -2012,6 +2005,8 @@ "@expo/cli/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], + "@expo/cli/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/cli/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/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], @@ -2026,18 +2021,26 @@ "@expo/config/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], + "@expo/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/config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@expo/config-plugins/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], + "@expo/config-plugins/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/config-plugins/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@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=="], + "@expo/env/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], "@expo/fingerprint/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], + "@expo/fingerprint/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/fingerprint/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@expo/image-utils/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], @@ -2048,10 +2051,14 @@ "@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/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/@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.5", "", {}, "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ=="], + "@expo/prebuild-config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@expo/xcpretty/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], @@ -2082,7 +2089,9 @@ "@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@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=="], @@ -2134,6 +2143,8 @@ "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=="], @@ -2152,6 +2163,8 @@ "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -2208,14 +2221,16 @@ "react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], - "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.5", "", {}, "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ=="], - "react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "react-native/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "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-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=="], @@ -2226,8 +2241,6 @@ "requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="], - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], @@ -2246,6 +2259,8 @@ "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "sucrase/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=="], + "tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], "tailwindcss/postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], @@ -2256,8 +2271,6 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -2302,7 +2315,9 @@ "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@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=="], @@ -2330,6 +2345,8 @@ "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "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=="], + "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=="], @@ -2360,12 +2377,8 @@ "node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "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=="], - "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -2408,7 +2421,7 @@ "@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=="], - "@react-native/codegen/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=="], + "@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=="], "ansi-fragments/slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -2428,10 +2441,6 @@ "metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "react-native/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=="], - - "rimraf/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=="], - "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=="], diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index 45a0f02d..fa9fd10b 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -90,7 +90,12 @@ export const DownloadItems: React.FC = ({ bottomSheetModalRef.current?.present(); }, []); - const handleSheetChanges = useCallback((_index: number) => {}, []); + const handleSheetChanges = useCallback((index: number) => { + // Ensure modal is fully dismissed when index is -1 + if (index === -1) { + // Modal is fully closed + } + }, []); const closeModal = useCallback(() => { bottomSheetModalRef.current?.dismiss(); @@ -245,14 +250,19 @@ export const DownloadItems: React.FC = ({ ], ); - const acceptDownloadOptions = useCallback(() => { + const acceptDownloadOptions = useCallback(async () => { if (userCanDownload === true) { if (itemsToDownload.some((i) => !i.Id)) { throw new Error("No item id"); } - closeModal(); - initiateDownload(...itemsToDownload); + // Ensure modal is dismissed before starting download + await closeModal(); + + // Small delay to ensure modal is fully dismissed + setTimeout(() => { + initiateDownload(...itemsToDownload); + }, 100); } else { toast.error( t("home.downloads.toasts.you_are_not_allowed_to_download_files"), @@ -326,7 +336,15 @@ export const DownloadItems: React.FC = ({ backgroundColor: "#171717", }} onChange={handleSheetChanges} + onDismiss={() => { + // Ensure any pending state is cleared when modal is dismissed + }} backdropComponent={renderBackdrop} + enablePanDownToClose + enableDismissOnClose + android_keyboardInputMode='adjustResize' + keyboardBehavior='interactive' + keyboardBlurBehavior='restore' > diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index d58d1bf2..85b2125b 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -176,8 +176,8 @@ export const ItemContent: React.FC = React.memo( style={{ height: 130, width: "100%", - resizeMode: "contain", }} + contentFit='contain' onLoad={() => setLoadingLogo(false)} onError={() => setLoadingLogo(false)} /> diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index e013a826..1bb730d9 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -125,6 +125,34 @@ export const PlayButton: React.FC = ({ // Check if user wants H265 for Chromecast const enableH265 = settings.enableH265ForChromecast; + // Validate required parameters before calling getStreamUrl + if (!api) { + console.warn("API not available for Chromecast streaming"); + Alert.alert( + t("player.client_error"), + t("player.missing_parameters"), + ); + return; + } + if (!user?.Id) { + console.warn( + "User not authenticated for Chromecast streaming", + ); + Alert.alert( + t("player.client_error"), + t("player.missing_parameters"), + ); + return; + } + if (!item?.Id) { + console.warn("Item not available for Chromecast streaming"); + Alert.alert( + t("player.client_error"), + t("player.missing_parameters"), + ); + return; + } + // Get a new URL with the Chromecast device profile try { const data = await getStreamUrl({ @@ -132,7 +160,7 @@ export const PlayButton: React.FC = ({ item, deviceProfile: enableH265 ? chromecasth265 : chromecast, startTimeTicks: item?.UserData?.PlaybackPositionTicks!, - userId: user?.Id, + userId: user.Id, audioStreamIndex: selectedOptions.audioIndex, maxStreamingBitrate: selectedOptions.bitrate?.value, mediaSourceId: selectedOptions.mediaSource?.Id, diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 447f06ba..eb3f38a4 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -1,5 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useQueryClient } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useRouter } from "expo-router"; import { t } from "i18next"; @@ -9,7 +9,6 @@ import { TouchableOpacity, type TouchableOpacityProps, View, - type ViewProps, } from "react-native"; import { toast } from "sonner-native"; import { Text } from "@/components/common/Text"; @@ -19,13 +18,13 @@ import { storage } from "@/utils/mmkv"; import { formatTimeString } from "@/utils/time"; import { Button } from "../Button"; -interface Props extends ViewProps {} - const bytesToMB = (bytes: number) => { return bytes / 1024 / 1024; }; -export const ActiveDownloads: React.FC = ({ ...props }) => { +interface ActiveDownloadsProps extends TouchableOpacityProps {} + +export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) { const { processes } = useDownload(); if (processes?.length === 0) return ( @@ -51,31 +50,48 @@ export const ActiveDownloads: React.FC = ({ ...props }) => { ); -}; +} interface DownloadCardProps extends TouchableOpacityProps { process: JobStatus; } const DownloadCard = ({ process, ...props }: DownloadCardProps) => { - const { startDownload, removeProcess } = useDownload(); + const { startDownload, pauseDownload, resumeDownload, removeProcess } = + useDownload(); const router = useRouter(); const queryClient = useQueryClient(); - const cancelJobMutation = useMutation({ - mutationFn: async (id: string) => { - if (!process) throw new Error("No active download"); - removeProcess(id); - }, - onSuccess: () => { - toast.success(t("home.downloads.toasts.download_cancelled")); + 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")); queryClient.invalidateQueries({ queryKey: ["downloads"] }); - }, - onError: (e) => { - console.error(e); - toast.error(t("home.downloads.toasts.could_not_cancel_download")); - }, - }); + } catch (error) { + console.error("Error deleting download:", error); + toast.error(t("home.downloads.toasts.could_not_delete_download")); + } + }; const eta = (p: JobStatus) => { if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null; @@ -121,8 +137,8 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { style={{ width: "100%", height: "100%", - resizeMode: "cover", }} + contentFit='cover' /> )} @@ -154,17 +170,30 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => { {process.status} - cancelJobMutation.mutate(process.id)} - className='ml-auto p-2 rounded-full' - > - {cancelJobMutation.isPending ? ( - - ) : ( - + + {process.status === "downloading" && ( + handlePause(process.id)} + className='p-2 rounded-full bg-yellow-600' + > + + )} - + {process.status === "paused" && ( + handleResume(process.id)} + className='p-2 rounded-full bg-green-600' + > + + + )} + handleDelete(process.id)} + className='p-2 rounded-full bg-red-600' + > + + + {process.status === "completed" && ( diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx index c193d562..e9c8ab97 100644 --- a/components/downloads/MovieCard.tsx +++ b/components/downloads/MovieCard.tsx @@ -77,8 +77,8 @@ export const MovieCard: React.FC = ({ item }) => { style={{ width: "100%", height: "100%", - resizeMode: "cover", }} + contentFit='cover' /> diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx index 0cc75150..8b2d4911 100644 --- a/components/downloads/SeriesCard.tsx +++ b/components/downloads/SeriesCard.tsx @@ -52,8 +52,8 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => { style={{ width: "100%", height: "100%", - resizeMode: "cover", }} + contentFit='cover' /> {items.length} diff --git a/components/home/Favorites.tsx b/components/home/Favorites.tsx index d434c333..b1b32d95 100644 --- a/components/home/Favorites.tsx +++ b/components/home/Favorites.tsx @@ -1,10 +1,11 @@ import type { Api } from "@jellyfin/sdk"; import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { Image } from "expo-image"; import { t } from "i18next"; import { useAtom } from "jotai"; import { useCallback, useEffect, useState } from "react"; -import { Image, Text, View } from "react-native"; +import { Text, View } from "react-native"; // PNG ASSET import heart from "@/assets/icons/heart.fill.png"; import { Colors } from "@/constants/Colors"; @@ -125,7 +126,8 @@ export const Favorites = () => { diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index 1e300576..80818d42 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -200,8 +200,8 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => { style={{ width: "100%", height: "100%", - resizeMode: "contain", }} + contentFit='contain' /> diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index 10695700..0f4c66a2 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -1,5 +1,6 @@ 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"; @@ -20,11 +21,6 @@ import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; -const BackgroundFetch = !Platform.isTV - ? require("expo-background-fetch") - : null; -const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; - export const OtherSettings: React.FC = () => { const router = useRouter(); const [settings, updateSettings, pluginSettings] = useSettings(); @@ -35,10 +31,8 @@ export const OtherSettings: React.FC = () => { * Background task *******************/ const checkStatusAsync = async () => { - if (Platform.isTV) return; - - await BackgroundFetch.getStatusAsync(); - return await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK); + if (Platform.isTV) return false; + return TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK); }; useEffect(() => { diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 65880958..7ca768af 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -1,3 +1,4 @@ +import { Api } from "@jellyfin/sdk"; import type { BaseItemDto, MediaSourceInfo, @@ -28,6 +29,7 @@ import { useIntroSkipper } from "@/hooks/useIntroSkipper"; import { usePlaybackManager } from "@/hooks/usePlaybackManager"; import { useTrickplay } from "@/hooks/useTrickplay"; import type { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types"; +import { DownloadedItem } from "@/providers/Downloads/types"; import { useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { ticksToMs } from "@/utils/time"; @@ -78,6 +80,8 @@ interface Props { setAspectRatio?: Dispatch>; setScaleFactor?: Dispatch>; isVlc?: boolean; + api?: Api | null; + downloadedFiles?: DownloadedItem[]; } export const Controls: FC = ({ @@ -109,8 +113,10 @@ export const Controls: FC = ({ setScaleFactor, offline = false, isVlc = false, + api = null, + downloadedFiles = undefined, }) => { - const [settings, updateSettings] = useSettings(); + const [settings, updateSettings] = useSettings(api); const router = useRouter(); const lightHapticFeedback = useHaptic("light"); @@ -321,6 +327,8 @@ export const Controls: FC = ({ play, isVlc, offline, + api, + downloadedFiles, ); const { showSkipCreditButton, skipCredit } = useCreditSkipper( @@ -330,6 +338,8 @@ export const Controls: FC = ({ play, isVlc, offline, + api, + downloadedFiles, ); const goToItemCommon = useCallback( diff --git a/components/video-player/controls/TrickplayBubble.tsx b/components/video-player/controls/TrickplayBubble.tsx index 28036d15..361a4c56 100644 --- a/components/video-player/controls/TrickplayBubble.tsx +++ b/components/video-player/controls/TrickplayBubble.tsx @@ -71,7 +71,6 @@ export const TrickplayBubble: FC = ({ { translateX: -x * tileWidth }, { translateY: -y * tileHeight }, ], - resizeMode: "cover", }} source={{ uri: url }} contentFit='cover' diff --git a/hooks/useCreditSkipper.ts b/hooks/useCreditSkipper.ts index 15231de4..d023e7be 100644 --- a/hooks/useCreditSkipper.ts +++ b/hooks/useCreditSkipper.ts @@ -1,4 +1,6 @@ +import { Api } from "@jellyfin/sdk"; import { useCallback, useEffect, useState } from "react"; +import { DownloadedItem } from "@/providers/Downloads/types"; import { useSegments } from "@/utils/segments"; import { msToSeconds, secondsToMs } from "@/utils/time"; import { useHaptic } from "./useHaptic"; @@ -10,6 +12,8 @@ export const useCreditSkipper = ( play: () => void, isVlc = false, isOffline = false, + api: Api | null = null, + downloadedFiles: DownloadedItem[] | undefined = undefined, ) => { const [showSkipCreditButton, setShowSkipCreditButton] = useState(false); const lightHapticFeedback = useHaptic("light"); @@ -26,7 +30,12 @@ export const useCreditSkipper = ( seek(seconds); }; - const { data: segments } = useSegments(itemId, isOffline); + const { data: segments } = useSegments( + itemId, + isOffline, + downloadedFiles, + api, + ); const creditTimestamps = segments?.creditSegments?.[0]; useEffect(() => { diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts index 2653a8e3..14004596 100644 --- a/hooks/useIntroSkipper.ts +++ b/hooks/useIntroSkipper.ts @@ -1,4 +1,6 @@ +import { Api } from "@jellyfin/sdk"; import { useCallback, useEffect, useState } from "react"; +import { DownloadedItem } from "@/providers/Downloads/types"; import { useSegments } from "@/utils/segments"; import { msToSeconds, secondsToMs } from "@/utils/time"; import { useHaptic } from "./useHaptic"; @@ -15,6 +17,8 @@ export const useIntroSkipper = ( play: () => void, isVlc = false, isOffline = false, + api: Api | null = null, + downloadedFiles: DownloadedItem[] | undefined = undefined, ) => { const [showSkipButton, setShowSkipButton] = useState(false); if (isVlc) { @@ -30,7 +34,12 @@ export const useIntroSkipper = ( seek(seconds); }; - const { data: segments } = useSegments(itemId, isOffline); + const { data: segments } = useSegments( + itemId, + isOffline, + downloadedFiles, + api, + ); const introTimestamps = segments?.introSegments?.[0]; useEffect(() => { diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index c5c681cc..c6b350c2 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -14,7 +14,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { t } from "i18next"; import { useCallback, useMemo } from "react"; import { toast } from "sonner-native"; -import { useSettings } from "@/utils/atoms/settings"; +import type { Settings } from "@/utils/atoms/settings"; import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; import { IssueStatus, @@ -416,9 +416,11 @@ export class JellyseerrApi { const jellyseerrUserAtom = atom(storage.get(JELLYSEERR_USER)); -export const useJellyseerr = () => { +export const useJellyseerr = ( + settings: Settings, + updateSettings: (update: Partial) => void, +) => { const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom); - const [settings, updateSettings] = useSettings(); const queryClient = useQueryClient(); const jellyseerrApi = useMemo(() => { @@ -472,7 +474,7 @@ export const useJellyseerr = () => { return ( items && Object.hasOwn(items, "mediaType") && - Object.values(MediaType).includes(items.mediaType) + Object.values(MediaType).includes(items.mediaType as MediaType) ); }; diff --git a/hooks/useTrickplay.ts b/hooks/useTrickplay.ts index 162fe144..5ebae53a 100644 --- a/hooks/useTrickplay.ts +++ b/hooks/useTrickplay.ts @@ -3,9 +3,12 @@ import { Image } from "expo-image"; import { useGlobalSearchParams } from "expo-router"; import { useCallback, useMemo, useRef, useState } from "react"; import { useDownload } from "@/providers/DownloadProvider"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import { store } from "@/utils/store"; import { ticksToMs } from "@/utils/time"; +import { + generateTrickplayUrl, + getTrickplayInfo, + type TrickplayInfo, +} from "@/utils/trickplay"; interface TrickplayUrl { x: number; @@ -15,8 +18,8 @@ interface TrickplayUrl { /** Hook to handle trickplay logic for a given item. */ export const useTrickplay = (item: BaseItemDto) => { - const [trickPlayUrl, setTrickPlayUrl] = useState(null); const { getDownloadedItemById } = useDownload(); + const [trickPlayUrl, setTrickPlayUrl] = useState(null); const lastCalculationTime = useRef(0); const throttleDelay = 200; const isOffline = useGlobalSearchParams().offline === "true"; @@ -33,7 +36,7 @@ export const useTrickplay = (item: BaseItemDto) => { } return generateTrickplayUrl(item, sheetIndex); }, - [trickplayInfo], + [trickplayInfo, isOffline, getDownloadedItemById], ); /** Calculates the trickplay URL for the current progress. */ @@ -57,12 +60,25 @@ export const useTrickplay = (item: BaseItemDto) => { [trickplayInfo, item, throttleDelay, getTrickplayUrl], ); - /** Prefetches all the trickplay images for the item. */ - const prefetchAllTrickplayImages = useCallback(() => { + /** Prefetches all the trickplay images for the item, limiting concurrency to avoid I/O spikes. */ + const prefetchAllTrickplayImages = useCallback(async () => { if (!trickplayInfo || !item.Id) return; - for (let index = 0; index < trickplayInfo.totalImageSheets; index++) { + const maxConcurrent = 4; + const total = trickplayInfo.totalImageSheets; + const urls: string[] = []; + for (let index = 0; index < total; index++) { const url = getTrickplayUrl(item, index); - if (url) Image.prefetch(url); + if (url) urls.push(url); + } + for (let i = 0; i < urls.length; i += maxConcurrent) { + const batch = urls.slice(i, i + maxConcurrent); + await Promise.all( + batch.map( + (url) => Image.prefetch(url).catch(() => {}), // Ignore errors + ), + ); + // Yield to the event loop between batches to avoid blocking + await Promise.resolve(); } }, [trickplayInfo, item, getTrickplayUrl]); @@ -83,67 +99,6 @@ export interface TrickplayData { ThumbnailCount?: number; } -export interface TrickplayInfo { - resolution: string; - aspectRatio: number; - data: TrickplayData; - totalImageSheets: number; -} - -/** Generates a trickplay URL based on the item, resolution, and sheet index. */ -export const generateTrickplayUrl = (item: BaseItemDto, sheetIndex: number) => { - const api = store.get(apiAtom); - const resolution = getTrickplayInfo(item)?.resolution; - if (!resolution || !api) return null; - return `${api.basePath}/Videos/${item.Id}/Trickplay/${resolution}/${sheetIndex}.jpg?api_key=${api.accessToken}`; -}; - -/** - * Parses the trickplay metadata from a BaseItemDto. - * @param item The Jellyfin media item. - * @returns Parsed trickplay information or null if not available. - */ -export const getTrickplayInfo = (item: BaseItemDto): TrickplayInfo | null => { - if (!item.Id || !item.Trickplay) return null; - - const mediaSourceId = item.Id; - const trickplayDataForSource = item.Trickplay[mediaSourceId]; - - if (!trickplayDataForSource) { - return null; - } - - const firstResolution = Object.keys(trickplayDataForSource)[0]; - if (!firstResolution) { - return null; - } - - const data = trickplayDataForSource[firstResolution]; - const { Interval, TileWidth, TileHeight, Width, Height } = data; - - if ( - !Interval || - !TileWidth || - !TileHeight || - !Width || - !Height || - !item.RunTimeTicks - ) { - return null; - } - - const tilesPerSheet = TileWidth * TileHeight; - const totalTiles = Math.ceil(ticksToMs(item.RunTimeTicks) / Interval); - const totalImageSheets = Math.ceil(totalTiles / tilesPerSheet); - - return { - resolution: firstResolution, - aspectRatio: Width / Height, - data, - totalImageSheets, - }; -}; - /** * Calculates the specific image sheet and tile offset for a given time. * @param progressTicks The current playback time in ticks. diff --git a/package.json b/package.json index 889b4728..06c14496 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease", "prepare": "husky", "typecheck": "tsc -p tsconfig.json --noEmit", - "check": "biome check . --max-diagnostics 1000 && bun run typecheck", + "check": "biome check . --max-diagnostics 1000", "lint": "biome check --write --unsafe --max-diagnostics 1000", "format": "biome format --write .", "doctor": "expo-doctor", @@ -23,7 +23,6 @@ }, "dependencies": { "@bottom-tabs/react-navigation": "^0.9.2", - "@expo/config-plugins": "~10.1.1", "@expo/metro-runtime": "~5.0.4", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^14.1.0", @@ -37,10 +36,10 @@ "@shopify/flash-list": "^1.8.3", "@tanstack/react-query": "^5.66.0", "axios": "^1.7.9", - "expo": "^53.0.6", + "expo": "^53.0.22", "expo-application": "~6.1.4", "expo-asset": "~11.1.7", - "expo-background-fetch": "~13.1.5", + "expo-background-task": "~0.2.8", "expo-blur": "~14.1.4", "expo-brightness": "~13.1.4", "expo-build-properties": "~0.14.6", @@ -54,14 +53,14 @@ "expo-linking": "~7.1.4", "expo-localization": "~16.1.5", "expo-notifications": "~0.31.2", - "expo-router": "~5.1.4", + "expo-router": "~5.1.5", "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.7", - "expo-task-manager": "~13.1.5", + "expo-system-ui": "~5.0.11", + "expo-task-manager": "~13.1.6", "expo-web-browser": "~14.2.0", "i18next": "^25.0.0", "jotai": "^2.12.5", @@ -84,7 +83,7 @@ "react-native-ios-utilities": "5.1.8", "react-native-mmkv": "2.12.2", "react-native-pager-view": "^6.9.1", - "react-native-reanimated": "~3.16.7", + "react-native-reanimated": "~3.17.4", "react-native-reanimated-carousel": "4.0.2", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", diff --git a/plugins/withAndroidManifest.js b/plugins/withAndroidManifest.js index 1896f813..883869fb 100644 --- a/plugins/withAndroidManifest.js +++ b/plugins/withAndroidManifest.js @@ -1,10 +1,8 @@ -const { - withAndroidManifest: NativeAndroidManifest, -} = require("@expo/config-plugins"); +const { withAndroidManifest } = require("expo/config-plugins"); -const withAndroidManifest = (config) => - NativeAndroidManifest(config, async (config) => { - const mainApplication = config.modResults.manifest.application[0]; +const _withGoogleCastAndroidManifest = (config) => + withAndroidManifest(config, async (mod) => { + const mainApplication = mod.modResults.manifest.application[0]; // Initialize activity array if it doesn't exist if (!mainApplication.activity) { @@ -25,6 +23,7 @@ const withAndroidManifest = (config) => "com.reactnative.googlecast.RNGCExpandedControllerActivity", "android:theme": "@style/Theme.MaterialComponents.NoActionBar", "android:launchMode": "singleTask", + "android:exported": "false", }, }); } @@ -33,11 +32,11 @@ const withAndroidManifest = (config) => (activity) => activity.$?.["android:name"] === ".MainActivity", ); - if (mainActivity) { + if (mainActivity?.$) { mainActivity.$["android:supportsPictureInPicture"] = "true"; } - return config; + return mod; }); -module.exports = withAndroidManifest; +module.exports = _withGoogleCastAndroidManifest; diff --git a/plugins/withChangeNativeAndroidTextToWhite.js b/plugins/withChangeNativeAndroidTextToWhite.js index aa4e3e60..efdb782b 100644 --- a/plugins/withChangeNativeAndroidTextToWhite.js +++ b/plugins/withChangeNativeAndroidTextToWhite.js @@ -1,6 +1,6 @@ const { readFileSync, writeFileSync } = require("node:fs"); const { join } = require("node:path"); -const { withDangerousMod } = require("@expo/config-plugins"); +const { withDangerousMod } = require("expo/config-plugins"); const withChangeNativeAndroidTextToWhite = (expoConfig) => withDangerousMod(expoConfig, [ diff --git a/plugins/withRNBackgroundDownloader.js b/plugins/withRNBackgroundDownloader.js index 47664561..83e1fb69 100644 --- a/plugins/withRNBackgroundDownloader.js +++ b/plugins/withRNBackgroundDownloader.js @@ -1,8 +1,8 @@ -const { withAppDelegate, withXcodeProject } = require("@expo/config-plugins"); +const { withAppDelegate, withXcodeProject } = require("expo/config-plugins"); const fs = require("node:fs"); const path = require("node:path"); -/** @param {import("@expo/config-plugins").ExpoConfig} config */ +/** @param {import("expo/config-plugins").ExpoConfig} config */ function withRNBackgroundDownloader(config) { /* 1️⃣ Add handleEventsForBackgroundURLSession to AppDelegate.swift */ config = withAppDelegate(config, (mod) => { diff --git a/plugins/withTrustLocalCerts.js b/plugins/withTrustLocalCerts.js index d0ff6314..20e902e4 100644 --- a/plugins/withTrustLocalCerts.js +++ b/plugins/withTrustLocalCerts.js @@ -1,5 +1,4 @@ -const { AndroidConfig, withAndroidManifest } = require("@expo/config-plugins"); -const { Paths } = require("@expo/config-plugins/build/android"); +const { AndroidConfig, withAndroidManifest } = require("expo/config-plugins"); const path = require("node:path"); const fs = require("node:fs"); const fsPromises = fs.promises; @@ -7,16 +6,18 @@ const fsPromises = fs.promises; const { getMainApplicationOrThrow } = AndroidConfig.Manifest; const withTrustLocalCerts = (config) => { - return withAndroidManifest(config, async (config) => { - config.modResults = await setCustomConfigAsync(config, config.modResults); - return config; + return withAndroidManifest(config, async (mod) => { + mod.modResults = await setCustomConfigAsync(mod, mod.modResults); + return mod; }); }; async function setCustomConfigAsync(config, androidManifest) { const src_file_path = path.join(__dirname, "network_security_config.xml"); const res_file_path = path.join( - await Paths.getResourceFolderAsync(config.modRequest.projectRoot), + await AndroidConfig.Paths.getResourceFolderAsync( + config.modRequest.projectRoot, + ), "xml", "network_security_config.xml", ); @@ -31,12 +32,15 @@ async function setCustomConfigAsync(config, androidManifest) { await fsPromises.copyFile(src_file_path, res_file_path); } catch (e) { throw new Error( - `Failed to copy network security config file from ${src_file_path} to ${res_file_path}: ${e.message}`, + `Failed to copy network security config file from ${src_file_path} to ${res_file_path}. [Hint: Check Android write permissions and file paths]`, + { cause: e }, ); } const mainApplication = getMainApplicationOrThrow(androidManifest); - mainApplication.$["android:networkSecurityConfig"] = - "@xml/network_security_config"; + if (!mainApplication.$["android:networkSecurityConfig"]) { + mainApplication.$["android:networkSecurityConfig"] = + "@xml/network_security_config"; + } return androidManifest; } diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 7a416890..3a8e9324 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -20,7 +20,6 @@ import { toast } from "sonner-native"; import { useHaptic } from "@/hooks/useHaptic"; import useImageStorage from "@/hooks/useImageStorage"; import { useInterval } from "@/hooks/useInterval"; -import { generateTrickplayUrl, getTrickplayInfo } from "@/hooks/useTrickplay"; import { useSettings } from "@/utils/atoms/settings"; import { getOrSetDeviceId } from "@/utils/device"; import useDownloadHelper from "@/utils/download"; @@ -28,6 +27,7 @@ import { getItemImage } from "@/utils/getItemImage"; import { 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, @@ -99,8 +99,8 @@ function useDownloadProvider() { // check if processes are missing setProcesses((processes) => { const missingProcesses = tasks - .filter((t) => t.metadata && !processes.some((p) => p.id === t.id)) - .map((t) => { + .filter((t: any) => t.metadata && !processes.some((p) => p.id === t.id)) + .map((t: any) => { return t.metadata as JobStatus; }); @@ -108,7 +108,7 @@ function useDownloadProvider() { const updatedProcesses = currentProcesses.map((p) => { // fallback. Doesn't really work for transcodes as they may be a lot smaller. // We make an wild guess by comparing bitrates - const task = tasks.find((s) => s.id === p.id); + const task = tasks.find((s: any) => s.id === p.id); if (task && p.status === "downloading") { const estimatedSize = calculateEstimatedSize(p); let progress = p.progress; @@ -425,7 +425,7 @@ function useDownloadProvider() { ); removeProcess(process.id); }) - .error((error) => { + .error((error: any) => { console.error("Download error:", error); toast.error( t("home.downloads.toasts.download_failed_for_item", { @@ -454,7 +454,7 @@ function useDownloadProvider() { const removeProcess = useCallback( async (id: string) => { const tasks = await BackGroundDownloader.checkForExistingDownloads(); - const task = tasks?.find((t) => t.id === id); + const task = tasks?.find((t: any) => t.id === id); task?.stop(); BackGroundDownloader.completeHandler(id); setProcesses((prev) => prev.filter((process) => process.id !== id)); @@ -690,6 +690,62 @@ function useDownloadProvider() { return { total, remaining, appSize: appSize }; }; + const pauseDownload = useCallback( + async (id: string) => { + const process = processes.find((p) => p.id === id); + if (!process) throw new Error("No active download"); + + const tasks = await BackGroundDownloader.checkForExistingDownloads(); + const task = tasks?.find((t: any) => t.id === id); + if (!task) throw new Error("No task found"); + + task.pause(); + updateProcess(id, { status: "paused" }); + }, + [processes, updateProcess], + ); + + const resumeDownload = useCallback( + async (id: string) => { + const process = processes.find((p) => p.id === id); + if (!process) throw new Error("No active download"); + + const tasks = await BackGroundDownloader.checkForExistingDownloads(); + const task = tasks?.find((t: any) => t.id === id); + if (!task) throw new Error("No task found"); + + // Check if task state allows resuming + if (task.state === "FAILED") { + console.warn( + "Download task failed, cannot resume. Restarting download.", + ); + // For failed tasks, we need to restart rather than resume + await startDownload(process); + return; + } + + try { + task.resume(); + updateProcess(id, { status: "downloading" }); + } catch (error: any) { + // Handle specific ERROR_CANNOT_RESUME error + if ( + error?.error === "ERROR_CANNOT_RESUME" || + error?.errorCode === 1008 + ) { + console.warn("Cannot resume download, attempting to restart instead"); + await startDownload(process); + return; // Return early to prevent error from bubbling up + } else { + // Only log error for non-handled cases + console.error("Error resuming download:", error); + throw error; // Re-throw other errors + } + } + }, + [processes, updateProcess, startDownload], + ); + return { processes, startBackgroundDownload, @@ -700,6 +756,8 @@ function useDownloadProvider() { deleteItems, removeProcess, startDownload, + pauseDownload, + resumeDownload, deleteFileByType, getDownloadedItemSize, getDownloadedItemById, @@ -725,6 +783,8 @@ export function useDownload() { deleteItems: async () => {}, removeProcess: () => {}, startDownload: async () => {}, + pauseDownload: async () => {}, + resumeDownload: async () => {}, deleteFileByType: async () => {}, getDownloadedItemSize: () => 0, getDownloadedItemById: () => undefined, diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 565813ea..f2f40d99 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -85,8 +85,11 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ _pluginSettings, setPluginSettings, refreshStreamyfinPluginSettings, - ] = useSettings(); - const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(); + ] = useSettings(api); + const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr( + _settings || {}, + _updateSettings, + ); const headers = useMemo(() => { if (!deviceId) return {}; diff --git a/translations/en.json b/translations/en.json index a03adb74..ade93f3c 100644 --- a/translations/en.json +++ b/translations/en.json @@ -270,8 +270,12 @@ "failed_to_delete_all_movies": "Failed to delete all movies", "deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!", "failed_to_delete_all_tvseries": "Failed to delete all TV-Series", - "download_cancelled": "Download cancelled", - "could_not_cancel_download": "Could not cancel download", + "download_deleted": "Download deleted", + "could_not_delete_download": "Could not delete download", + "download_paused": "Download paused", + "could_not_pause_download": "Could not pause download", + "download_resumed": "Download resumed", + "could_not_resume_download": "Could not resume download", "download_completed": "Download completed", "download_started_for": "Download started for {{item}}", "item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 2c1746f5..e2053543 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -1,3 +1,4 @@ +import type { Api } from "@jellyfin/sdk"; import { type BaseItemKind, type CultureDto, @@ -6,12 +7,11 @@ import { type SortOrder, SubtitlePlaybackMode, } from "@jellyfin/sdk/lib/generated-client"; -import { atom, useAtom, useAtomValue } from "jotai"; +import { atom, useAtom } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; import { Platform } from "react-native"; import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; import * as ScreenOrientation from "@/packages/expo-screen-orientation"; -import { apiAtom } from "@/providers/JellyfinProvider"; import { writeInfoLog } from "@/utils/log"; import { storage } from "../mmkv"; @@ -276,8 +276,7 @@ export const pluginSettingsAtom = atom( loadPluginSettings(), ); -export const useSettings = () => { - const api = useAtomValue(apiAtom); +export const useSettings = (api: Api | null) => { const [_settings, setSettings] = useAtom(settingsAtom); const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); @@ -301,11 +300,11 @@ export const useSettings = () => { return; } const settings = await api.getStreamyfinPluginConfig().then( - ({ data }) => { + ({ data }: any) => { writeInfoLog("Got plugin settings", data?.settings); return data?.settings; }, - (_err) => undefined, + (_err: any) => undefined, ); setPluginSettings(settings); return settings; diff --git a/utils/background-tasks.ts b/utils/background-tasks.ts index 29fb8671..7ba7b05f 100644 --- a/utils/background-tasks.ts +++ b/utils/background-tasks.ts @@ -1,50 +1,110 @@ +import * as BackgroundTask from "expo-background-task"; +import * as TaskManager from "expo-task-manager"; import { Platform } from "react-native"; +import { writeErrorLog } from "@/utils/log"; -const BackgroundFetch = !Platform.isTV - ? require("expo-background-fetch") - : null; +const BackgroundTaskModule = !Platform.isTV ? BackgroundTask : null; export const BACKGROUND_FETCH_TASK = "background-fetch"; - -export async function registerBackgroundFetchAsync() { - try { - BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { - minimumInterval: 60 * 1, // 1 minutes - stopOnTerminate: false, // android only, - startOnBoot: false, // android only - }); - } catch (error) { - console.log("Error registering background fetch task", error); - } -} - -export async function unregisterBackgroundFetchAsync() { - try { - BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK); - } catch (error) { - console.log("Error unregistering background fetch task", error); - } -} - export const BACKGROUND_FETCH_TASK_SESSIONS = "background-fetch-sessions"; -export async function registerBackgroundFetchAsyncSessions() { +export async function registerBackgroundFetchAsync(): Promise { + if (!BackgroundTaskModule) { + console.log( + "BackgroundTask module not available (TV platform or not supported)", + ); + return false; + } + try { - console.log("Registering background fetch sessions"); - BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS, { - minimumInterval: 1 * 60, // 1 minutes - stopOnTerminate: false, // android only, - startOnBoot: true, // android only + // Check if task is already registered + const isRegistered = await TaskManager.isTaskRegisteredAsync( + BACKGROUND_FETCH_TASK, + ); + if (isRegistered) { + console.log("Background fetch task already registered"); + return true; + } + + await BackgroundTaskModule!.unregisterTaskAsync(BACKGROUND_FETCH_TASK); + const minimumInterval = Platform.OS === "android" ? 600 : 900; + await BackgroundTaskModule!.registerTaskAsync(BACKGROUND_FETCH_TASK, { + minimumInterval, }); + console.log("Successfully registered background fetch task"); + return true; } catch (error) { - console.log("Error registering background fetch task", error); + // Log error but don't throw - background fetch is not critical + console.warn("Failed to register background fetch task:", error); + writeErrorLog("Error registering background fetch task", error); + return false; } } -export async function unregisterBackgroundFetchAsyncSessions() { +export async function unregisterBackgroundFetchAsync(): Promise { + if (!BackgroundTaskModule) return false; try { - BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS); + await BackgroundTaskModule!.unregisterTaskAsync(BACKGROUND_FETCH_TASK); + console.log("Successfully unregistered background fetch task"); + return true; } catch (error) { - console.log("Error unregistering background fetch task", error); + // Log error but don't throw - unregistering is not critical + console.warn("Failed to unregister background fetch task:", error); + writeErrorLog("Error unregistering background fetch task", error); + return false; + } +} + +export async function unregisterBackgroundFetchAsyncSessions(): Promise { + if (!BackgroundTaskModule) return false; + try { + await BackgroundTaskModule!.unregisterTaskAsync( + BACKGROUND_FETCH_TASK_SESSIONS, + ); + console.log("Successfully unregistered background fetch sessions task"); + return true; + } catch (error) { + // Log error but don't throw - unregistering is not critical + console.warn("Failed to unregister background fetch sessions task:", error); + writeErrorLog("Error unregistering background fetch sessions task", error); + return false; + } +} + +export async function registerBackgroundFetchAsyncSessions(): Promise { + if (!BackgroundTaskModule) { + console.log( + "BackgroundTask module not available (TV platform or not supported)", + ); + return false; + } + + try { + // Check if task is already registered + const isRegistered = await TaskManager.isTaskRegisteredAsync( + BACKGROUND_FETCH_TASK_SESSIONS, + ); + if (isRegistered) { + console.log("Background fetch sessions task already registered"); + return true; + } + + await BackgroundTaskModule!.unregisterTaskAsync( + BACKGROUND_FETCH_TASK_SESSIONS, + ); + const minimumInterval = Platform.OS === "android" ? 600 : 900; + await BackgroundTaskModule!.registerTaskAsync( + BACKGROUND_FETCH_TASK_SESSIONS, + { + minimumInterval, + }, + ); + console.log("Successfully registered background fetch sessions task"); + return true; + } catch (error) { + // Log error but don't throw - background fetch is not critical + console.warn("Failed to register background fetch sessions task:", error); + writeErrorLog("Error registering background fetch sessions task", error); + return false; } } diff --git a/utils/segments.ts b/utils/segments.ts index 5c36de78..136eb98e 100644 --- a/utils/segments.ts +++ b/utils/segments.ts @@ -1,9 +1,7 @@ import { Api } from "@jellyfin/sdk"; import { useQuery } from "@tanstack/react-query"; -import { useAtom } from "jotai"; -import { useDownload } from "@/providers/DownloadProvider"; +import React from "react"; import { DownloadedItem, MediaTimeSegment } from "@/providers/Downloads/types"; -import { apiAtom } from "@/providers/JellyfinProvider"; import { getAuthHeaders } from "./jellyfin/jellyfin"; interface IntroTimestamps { @@ -28,11 +26,16 @@ interface CreditTimestamps { }; } -export const useSegments = (itemId: string, isOffline: boolean) => { - const [api] = useAtom(apiAtom); - const { downloadedFiles } = useDownload(); - const downloadedItem = downloadedFiles?.find( - (d: DownloadedItem) => d.item.Id === itemId, +export const useSegments = ( + itemId: string, + isOffline: boolean, + downloadedFiles: DownloadedItem[] | undefined, + api: Api | null, +) => { + // Memoize the lookup so the array is only traversed when dependencies change + const downloadedItem = React.useMemo( + () => downloadedFiles?.find((d) => d.item.Id === itemId), + [downloadedFiles, itemId], ); return useQuery({ @@ -46,7 +49,7 @@ export const useSegments = (itemId: string, isOffline: boolean) => { } return fetchAndParseSegments(itemId, api); }, - enabled: !!api, + enabled: isOffline ? !!downloadedItem : !!api, }); }; @@ -76,15 +79,11 @@ export const fetchAndParseSegments = async ( const [introRes, creditRes] = await Promise.allSettled([ api.axiosInstance.get( `${api.basePath}/Episode/${itemId}/IntroTimestamps`, - { - headers: getAuthHeaders(api), - }, + { headers: getAuthHeaders(api) }, ), api.axiosInstance.get( `${api.basePath}/Episode/${itemId}/Timestamps`, - { - headers: getAuthHeaders(api), - }, + { headers: getAuthHeaders(api) }, ), ]); diff --git a/utils/trickplay.ts b/utils/trickplay.ts new file mode 100644 index 00000000..aeb19bc9 --- /dev/null +++ b/utils/trickplay.ts @@ -0,0 +1,65 @@ +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { store } from "@/utils/store"; +import { ticksToMs } from "@/utils/time"; + +export interface TrickplayInfo { + resolution: string; + aspectRatio: number; + data: any; + totalImageSheets: number; +} + +/** + * Parses the trickplay metadata from a BaseItemDto. + * @param item The Jellyfin media item. + * @returns Parsed trickplay information or null if not available. + */ +export const getTrickplayInfo = (item: BaseItemDto): TrickplayInfo | null => { + if (!item.Id || !item.Trickplay) return null; + + const mediaSourceId = item.Id; + const trickplayDataForSource = item.Trickplay[mediaSourceId]; + + if (!trickplayDataForSource) { + return null; + } + + const firstResolution = Object.keys(trickplayDataForSource)[0]; + if (!firstResolution) { + return null; + } + + const data = trickplayDataForSource[firstResolution]; + const { Interval, TileWidth, TileHeight, Width, Height } = data; + + if ( + !Interval || + !TileWidth || + !TileHeight || + !Width || + !Height || + !item.RunTimeTicks + ) { + return null; + } + + const tilesPerSheet = TileWidth * TileHeight; + const totalTiles = Math.ceil(ticksToMs(item.RunTimeTicks) / Interval); + const totalImageSheets = Math.ceil(totalTiles / tilesPerSheet); + + return { + resolution: firstResolution, + aspectRatio: Width / Height, + data, + totalImageSheets, + }; +}; + +/** Generates a trickplay URL based on the item, resolution, and sheet index. */ +export const generateTrickplayUrl = (item: BaseItemDto, sheetIndex: number) => { + const api = store.get(apiAtom); + const resolution = getTrickplayInfo(item)?.resolution; + if (!resolution || !api) return null; + return `${api.basePath}/Videos/${item.Id}/Trickplay/${resolution}/${sheetIndex}.jpg?api_key=${api.accessToken}`; +};