mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
Compare commits
43 Commits
feat/disab
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36304ad58e | ||
|
|
baeb83581e | ||
|
|
05b7a4c50d | ||
|
|
28b67f3ad6 | ||
|
|
51cd195bfe | ||
|
|
0184e266a0 | ||
|
|
ae658aa5b0 | ||
|
|
81f79a54af | ||
|
|
ca1b640a61 | ||
|
|
e771949c95 | ||
|
|
78bfa68a17 | ||
|
|
ac59615d79 | ||
|
|
4dd80cd8f5 | ||
|
|
db9f02b225 | ||
|
|
7a0bbb1084 | ||
|
|
05925530c0 | ||
|
|
625a292e26 | ||
|
|
1acd3102ea | ||
|
|
543881dc41 | ||
|
|
5d93483dc2 | ||
|
|
d54a29020a | ||
|
|
1d04e39b85 | ||
|
|
ecc62259fc | ||
|
|
ffd96e05fe | ||
|
|
8541ba02d4 | ||
|
|
6c955d8a2a | ||
|
|
b0bb6c6c9a | ||
|
|
82abc291d4 | ||
|
|
3da4b42ca3 | ||
|
|
16940075b2 | ||
|
|
a3bbb1bc3a | ||
|
|
1874c116a6 | ||
|
|
7a0f70778d | ||
|
|
6957c4fd64 | ||
|
|
1c0ed82deb | ||
|
|
ad54823f96 | ||
|
|
cfa638afc6 | ||
|
|
467bea7192 | ||
|
|
ac9ac5d423 | ||
|
|
62b45121e5 | ||
|
|
0e238ad10e | ||
|
|
ce793e3469 | ||
|
|
beba4853b9 |
@@ -24,4 +24,4 @@ This file is auto-imported into CLAUDE.md and loaded at the start of each sessio
|
|||||||
|
|
||||||
- **Mark as played flow**: The "mark as played" button uses `PlayedStatus` component → `useMarkAsPlayed` hook → `usePlaybackManager.markItemPlayed()`. The hook does optimistic updates via `setQueriesData` before calling the API. Located in `components/PlayedStatus.tsx` and `hooks/useMarkAsPlayed.ts`. _(2026-01-10)_
|
- **Mark as played flow**: The "mark as played" button uses `PlayedStatus` component → `useMarkAsPlayed` hook → `usePlaybackManager.markItemPlayed()`. The hook does optimistic updates via `setQueriesData` before calling the API. Located in `components/PlayedStatus.tsx` and `hooks/useMarkAsPlayed.ts`. _(2026-01-10)_
|
||||||
|
|
||||||
- **Stack screen header configuration**: Sub-pages under `(home)` need explicit `Stack.Screen` entries in `app/(auth)/(tabs)/(home)/_layout.tsx` with `headerTransparent: Platform.OS === "ios"`, `headerBlurEffect: "none"`, and a back button. Without this, pages show with wrong header styling. _(2026-01-10)_
|
- **Stack screen header configuration**: Sub-pages under `(home)` need explicit `Stack.Screen` entries in `app/(auth)/(tabs)/(home)/_layout.tsx` with `headerTransparent: Platform.OS === "ios"`, `headerBlurEffect: "none"`, and a back button. Without this, pages show with wrong header styling. _(2026-01-10)_
|
||||||
89
.github/workflows/build-apps.yml
vendored
89
.github/workflows/build-apps.yml
vendored
@@ -20,6 +20,18 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: 🗑️ Free Disk Space
|
||||||
|
uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3
|
||||||
|
with:
|
||||||
|
tool-cache: false
|
||||||
|
mandb: true
|
||||||
|
android: false
|
||||||
|
dotnet: true
|
||||||
|
haskell: true
|
||||||
|
large-packages: true
|
||||||
|
docker-images: true
|
||||||
|
swap-storage: false
|
||||||
|
|
||||||
- name: 📥 Checkout code
|
- name: 📥 Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
@@ -91,6 +103,18 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: 🗑️ Free Disk Space
|
||||||
|
uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3
|
||||||
|
with:
|
||||||
|
tool-cache: false
|
||||||
|
mandb: true
|
||||||
|
android: false
|
||||||
|
dotnet: true
|
||||||
|
haskell: true
|
||||||
|
large-packages: true
|
||||||
|
docker-images: true
|
||||||
|
swap-storage: false
|
||||||
|
|
||||||
- name: 📥 Checkout code
|
- name: 📥 Checkout code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
@@ -194,7 +218,7 @@ jobs:
|
|||||||
- name: 🔧 Setup Xcode
|
- name: 🔧 Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
|
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
|
||||||
with:
|
with:
|
||||||
xcode-version: "26.0.1"
|
xcode-version: "26.2"
|
||||||
|
|
||||||
- name: 🏗️ Setup EAS
|
- name: 🏗️ Setup EAS
|
||||||
uses: expo/expo-github-action@main
|
uses: expo/expo-github-action@main
|
||||||
@@ -203,9 +227,6 @@ jobs:
|
|||||||
token: ${{ secrets.EXPO_TOKEN }}
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
eas-cache: true
|
eas-cache: true
|
||||||
|
|
||||||
- name: ⚙️ Ensure iOS SDKs installed
|
|
||||||
run: xcodebuild -downloadPlatform iOS
|
|
||||||
|
|
||||||
- name: 🚀 Build iOS app
|
- name: 🚀 Build iOS app
|
||||||
env:
|
env:
|
||||||
EXPO_TV: 0
|
EXPO_TV: 0
|
||||||
@@ -221,6 +242,63 @@ jobs:
|
|||||||
path: build-*.ipa
|
path: build-*.ipa
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
build-ios-phone-unsigned:
|
||||||
|
if: (!contains(github.event.head_commit.message, '[skip ci]'))
|
||||||
|
runs-on: macos-26
|
||||||
|
name: 🍎 Build iOS IPA (Phone - Unsigned)
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout code
|
||||||
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: 🍞 Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: 💾 Cache Bun dependencies
|
||||||
|
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||||
|
with:
|
||||||
|
path: ~/.bun/install/cache
|
||||||
|
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-bun-cache
|
||||||
|
|
||||||
|
- name: 📦 Install dependencies and reload submodules
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run submodule-reload
|
||||||
|
|
||||||
|
- name: 🛠️ Generate project files
|
||||||
|
run: bun run prebuild
|
||||||
|
|
||||||
|
- name: 🔧 Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
|
||||||
|
with:
|
||||||
|
xcode-version: "26.2"
|
||||||
|
|
||||||
|
- name: 🚀 Build iOS app
|
||||||
|
env:
|
||||||
|
EXPO_TV: 0
|
||||||
|
run: bun run ios:unsigned-build ${{ github.event_name == 'pull_request' && '-- --verbose' || '' }}
|
||||||
|
|
||||||
|
- name: 📅 Set date tag
|
||||||
|
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 📤 Upload IPA artifact
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
|
with:
|
||||||
|
name: streamyfin-ios-phone-unsigned-ipa-${{ env.DATE_TAG }}
|
||||||
|
path: build/*.ipa
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
# Disabled for now - uncomment when ready to build iOS TV
|
# Disabled for now - uncomment when ready to build iOS TV
|
||||||
# build-ios-tv:
|
# build-ios-tv:
|
||||||
# if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
|
# if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
|
||||||
@@ -271,9 +349,6 @@ jobs:
|
|||||||
# token: ${{ secrets.EXPO_TOKEN }}
|
# token: ${{ secrets.EXPO_TOKEN }}
|
||||||
# eas-cache: true
|
# eas-cache: true
|
||||||
#
|
#
|
||||||
# - name: ⚙️ Ensure tvOS SDKs installed
|
|
||||||
# run: xcodebuild -downloadPlatform tvOS
|
|
||||||
#
|
|
||||||
# - name: 🚀 Build iOS app
|
# - name: 🚀 Build iOS app
|
||||||
# env:
|
# env:
|
||||||
# EXPO_TV: 1
|
# EXPO_TV: 1
|
||||||
|
|||||||
6
.github/workflows/ci-codeql.yml
vendored
6
.github/workflows/ci-codeql.yml
vendored
@@ -27,13 +27,13 @@ jobs:
|
|||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: 🏁 Initialize CodeQL
|
- name: 🏁 Initialize CodeQL
|
||||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended,security-and-quality
|
queries: +security-extended,security-and-quality
|
||||||
|
|
||||||
- name: 🛠️ Autobuild
|
- name: 🛠️ Autobuild
|
||||||
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||||
|
|
||||||
- name: 🧪 Perform CodeQL Analysis
|
- name: 🧪 Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||||
|
|||||||
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@@ -107,7 +107,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: "🟢 Setup Node.js"
|
- name: "🟢 Setup Node.js"
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.x'
|
node-version: '24.x'
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/notification.yml
vendored
1
.github/workflows/notification.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
name: 🛎️ Discord Notification
|
name: 🛎️ Discord Notification
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
2
.github/workflows/update-issue-form.yml
vendored
2
.github/workflows/update-issue-form.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: "🟢 Setup Node.js"
|
- name: "🟢 Setup Node.js"
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: '24.x'
|
node-version: '24.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -66,7 +66,10 @@ streamyfin-4fec1-firebase-adminsdk.json
|
|||||||
|
|
||||||
# Version and Backup Files
|
# Version and Backup Files
|
||||||
/version-backup-*
|
/version-backup-*
|
||||||
modules/background-downloader/android/build/*
|
|
||||||
/modules/sf-player/android/build
|
/modules/sf-player/android/build
|
||||||
/modules/music-controls/android/build
|
/modules/music-controls/android/build
|
||||||
|
modules/background-downloader/android/build/*
|
||||||
/modules/mpv-player/android/build
|
/modules/mpv-player/android/build
|
||||||
|
|
||||||
|
# ios:unsigned-build Artifacts
|
||||||
|
build/
|
||||||
15
CLAUDE.md
15
CLAUDE.md
@@ -77,6 +77,21 @@ bun run ios:install-metal-toolchain # Fix "missing Metal Toolchain" build error
|
|||||||
- File-based routing in `app/` directory
|
- File-based routing in `app/` directory
|
||||||
- Tab navigation: `(home)`, `(search)`, `(favorites)`, `(libraries)`, `(watchlists)`
|
- Tab navigation: `(home)`, `(search)`, `(favorites)`, `(libraries)`, `(watchlists)`
|
||||||
- Shared routes use parenthesized groups like `(home,libraries,search,favorites,watchlists)`
|
- Shared routes use parenthesized groups like `(home,libraries,search,favorites,watchlists)`
|
||||||
|
- **IMPORTANT**: Always use `useAppRouter` from `@/hooks/useAppRouter` instead of `useRouter` from `expo-router`. This custom hook automatically handles offline mode state preservation across navigation:
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ❌ Never use this
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Offline Mode**:
|
||||||
|
- Use `OfflineModeProvider` from `@/providers/OfflineModeProvider` to wrap pages that support offline content
|
||||||
|
- Use `useOfflineMode()` hook to check if current context is offline
|
||||||
|
- The `useAppRouter` hook automatically injects `offline=true` param when navigating within an offline context
|
||||||
|
|
||||||
**Providers** (wrapping order in `app/_layout.tsx`):
|
**Providers** (wrapping order in `app/_layout.tsx`):
|
||||||
1. JotaiProvider
|
1. JotaiProvider
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
<img src="https://raw.githubusercontent.com/streamyfin/.github/refs/heads/main/streamyfin-github-banner.png" alt="Streamyfin" width="100%">
|
<img src="https://raw.githubusercontent.com/streamyfin/.github/refs/heads/main/streamyfin-github-banner.png" alt="Streamyfin" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://discord.gg/aJvAYeycyY">
|
||||||
|
<img alt="Streamyfin Discord" src="https://img.shields.io/badge/Discord-Streamyfin-blue?style=flat-square&logo=discord">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**Streamyfin is a user-friendly Jellyfin video streaming client built with Expo. Designed as an alternative to other Jellyfin clients, it aims to offer a smooth and reliable streaming experience. We hope you'll find it a valuable addition to your media streaming toolbox.**
|
**Streamyfin is a user-friendly Jellyfin video streaming client built with Expo. Designed as an alternative to other Jellyfin clients, it aims to offer a smooth and reliable streaming experience. We hope you'll find it a valuable addition to your media streaming toolbox.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
7
app.json
7
app.json
@@ -17,6 +17,7 @@
|
|||||||
"NSMicrophoneUsageDescription": "The app needs access to your microphone.",
|
"NSMicrophoneUsageDescription": "The app needs access to your microphone.",
|
||||||
"UIBackgroundModes": ["audio", "fetch"],
|
"UIBackgroundModes": ["audio", "fetch"],
|
||||||
"NSLocalNetworkUsageDescription": "The app needs access to your local network to connect to your Jellyfin server.",
|
"NSLocalNetworkUsageDescription": "The app needs access to your local network to connect to your Jellyfin server.",
|
||||||
|
"NSLocationWhenInUseUsageDescription": "Streamyfin uses your location to detect your home WiFi network for automatic local server switching.",
|
||||||
"NSAppTransportSecurity": {
|
"NSAppTransportSecurity": {
|
||||||
"NSAllowsArbitraryLoads": true
|
"NSAllowsArbitraryLoads": true
|
||||||
},
|
},
|
||||||
@@ -28,6 +29,9 @@
|
|||||||
"usesNonExemptEncryption": false
|
"usesNonExemptEncryption": false
|
||||||
},
|
},
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
|
"entitlements": {
|
||||||
|
"com.apple.developer.networking.wifi-info": true
|
||||||
|
},
|
||||||
"bundleIdentifier": "com.fredrikburmester.streamyfin",
|
"bundleIdentifier": "com.fredrikburmester.streamyfin",
|
||||||
"icon": "./assets/images/icon-ios-liquid-glass.icon",
|
"icon": "./assets/images/icon-ios-liquid-glass.icon",
|
||||||
"appleTeamId": "MWD5K362T8"
|
"appleTeamId": "MWD5K362T8"
|
||||||
@@ -44,7 +48,8 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"android.permission.FOREGROUND_SERVICE",
|
"android.permission.FOREGROUND_SERVICE",
|
||||||
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
|
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
|
||||||
"android.permission.WRITE_SETTINGS"
|
"android.permission.WRITE_SETTINGS",
|
||||||
|
"android.permission.ACCESS_FINE_LOCATION"
|
||||||
],
|
],
|
||||||
"blockedPermissions": ["android.permission.ACTIVITY_RECOGNITION"],
|
"blockedPermissions": ["android.permission.ACTIVITY_RECOGNITION"],
|
||||||
"googleServicesFile": "./google-services.json"
|
"googleServicesFile": "./google-services.json"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { Pressable } from "react-native-gesture-handler";
|
import { Pressable } from "react-native-gesture-handler";
|
||||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
|
||||||
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
|
const Chromecast = Platform.isTV ? null : require("@/components/Chromecast");
|
||||||
|
|
||||||
@@ -57,25 +58,6 @@ export default function IndexLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name='downloads/[seriesId]'
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
headerBlurEffect: "none",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
headerShadowVisible: false,
|
|
||||||
title: t("home.downloads.tvseries"),
|
|
||||||
headerLeft: () => (
|
|
||||||
<Pressable
|
|
||||||
onPress={() => _router.back()}
|
|
||||||
className='pl-0.5'
|
|
||||||
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
|
||||||
>
|
|
||||||
<Feather name='chevron-left' size={28} color='white' />
|
|
||||||
</Pressable>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='sessions/index'
|
name='sessions/index'
|
||||||
options={{
|
options={{
|
||||||
@@ -329,6 +311,24 @@ export default function IndexLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='settings/network/page'
|
||||||
|
options={{
|
||||||
|
title: t("home.settings.network.title"),
|
||||||
|
headerBlurEffect: "none",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
headerShadowVisible: false,
|
||||||
|
headerLeft: () => (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => _router.back()}
|
||||||
|
className='pl-0.5'
|
||||||
|
style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
|
||||||
|
>
|
||||||
|
<Feather name='chevron-left' size={28} color='white' />
|
||||||
|
</Pressable>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||||
<Stack.Screen key={name} name={name} options={options} />
|
<Stack.Screen key={name} name={name} options={options} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { FlashList } from "@shopify/flash-list";
|
|
||||||
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { Alert, Platform, TouchableOpacity, View } from "react-native";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { EpisodeCard } from "@/components/downloads/EpisodeCard";
|
|
||||||
import {
|
|
||||||
SeasonDropdown,
|
|
||||||
type SeasonIndexState,
|
|
||||||
} from "@/components/series/SeasonDropdown";
|
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
|
||||||
import { storage } from "@/utils/mmkv";
|
|
||||||
|
|
||||||
export default function page() {
|
|
||||||
const navigation = useNavigation();
|
|
||||||
const local = useLocalSearchParams();
|
|
||||||
const { seriesId, episodeSeasonIndex } = local as {
|
|
||||||
seriesId: string;
|
|
||||||
episodeSeasonIndex: number | string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [seasonIndexState, setSeasonIndexState] = useState<SeasonIndexState>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
const { downloadedItems, deleteItems } = useDownload();
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
const series = useMemo(() => {
|
|
||||||
try {
|
|
||||||
return (
|
|
||||||
downloadedItems
|
|
||||||
?.filter((f) => f.item.SeriesId === seriesId)
|
|
||||||
?.sort(
|
|
||||||
(a, b) =>
|
|
||||||
(a.item.ParentIndexNumber ?? 0) - (b.item.ParentIndexNumber ?? 0),
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}, [downloadedItems, seriesId]);
|
|
||||||
|
|
||||||
// Group episodes by season in a single pass
|
|
||||||
const seasonGroups = useMemo(() => {
|
|
||||||
const groups: Record<number, BaseItemDto[]> = {};
|
|
||||||
|
|
||||||
series.forEach((episode) => {
|
|
||||||
const seasonNumber = episode.item.ParentIndexNumber;
|
|
||||||
if (seasonNumber !== undefined && seasonNumber !== null) {
|
|
||||||
if (!groups[seasonNumber]) {
|
|
||||||
groups[seasonNumber] = [];
|
|
||||||
}
|
|
||||||
groups[seasonNumber].push(episode.item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort episodes within each season
|
|
||||||
Object.values(groups).forEach((episodes) => {
|
|
||||||
episodes.sort((a, b) => (a.IndexNumber || 0) - (b.IndexNumber || 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}, [series]);
|
|
||||||
|
|
||||||
// Get unique seasons (just the season numbers, sorted)
|
|
||||||
const uniqueSeasons = useMemo(() => {
|
|
||||||
const seasonNumbers = Object.keys(seasonGroups)
|
|
||||||
.map(Number)
|
|
||||||
.sort((a, b) => a - b);
|
|
||||||
return seasonNumbers.map((seasonNum) => seasonGroups[seasonNum][0]); // First episode of each season
|
|
||||||
}, [seasonGroups]);
|
|
||||||
|
|
||||||
const seasonIndex =
|
|
||||||
seasonIndexState[series?.[0]?.item?.ParentId ?? ""] ??
|
|
||||||
episodeSeasonIndex ??
|
|
||||||
series?.[0]?.item?.ParentIndexNumber ??
|
|
||||||
"";
|
|
||||||
|
|
||||||
const groupBySeason = useMemo<BaseItemDto[]>(() => {
|
|
||||||
return seasonGroups[Number(seasonIndex)] ?? [];
|
|
||||||
}, [seasonGroups, seasonIndex]);
|
|
||||||
|
|
||||||
const initialSeasonIndex = useMemo(
|
|
||||||
() =>
|
|
||||||
groupBySeason?.[0]?.ParentIndexNumber ??
|
|
||||||
series?.[0]?.item?.ParentIndexNumber,
|
|
||||||
[groupBySeason, series],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (series.length > 0) {
|
|
||||||
navigation.setOptions({
|
|
||||||
title: series[0].item.SeriesName,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
storage.remove(seriesId);
|
|
||||||
router.back();
|
|
||||||
}
|
|
||||||
}, [series]);
|
|
||||||
|
|
||||||
const deleteSeries = useCallback(() => {
|
|
||||||
Alert.alert(
|
|
||||||
"Delete season",
|
|
||||||
"Are you sure you want to delete the entire season?",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: "Cancel",
|
|
||||||
style: "cancel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Delete",
|
|
||||||
onPress: () =>
|
|
||||||
deleteItems(
|
|
||||||
groupBySeason
|
|
||||||
.map((item) => item.Id)
|
|
||||||
.filter((id) => id !== undefined),
|
|
||||||
),
|
|
||||||
style: "destructive",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}, [groupBySeason, deleteItems]);
|
|
||||||
|
|
||||||
const ListHeaderComponent = useCallback(() => {
|
|
||||||
if (series.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='flex flex-row items-center justify-start pb-2'>
|
|
||||||
<SeasonDropdown
|
|
||||||
item={series[0].item}
|
|
||||||
seasons={uniqueSeasons}
|
|
||||||
state={seasonIndexState}
|
|
||||||
initialSeasonIndex={initialSeasonIndex!}
|
|
||||||
onSelect={(season) => {
|
|
||||||
setSeasonIndexState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[series[0].item.ParentId ?? ""]: season.ParentIndexNumber,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center ml-2'>
|
|
||||||
<Text className='text-xs font-bold'>{groupBySeason.length}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='bg-neutral-800/80 rounded-full h-9 w-9 flex items-center justify-center ml-auto'>
|
|
||||||
<TouchableOpacity onPress={deleteSeries}>
|
|
||||||
<Ionicons name='trash' size={20} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
series,
|
|
||||||
uniqueSeasons,
|
|
||||||
seasonIndexState,
|
|
||||||
initialSeasonIndex,
|
|
||||||
groupBySeason,
|
|
||||||
deleteSeries,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='flex-1'>
|
|
||||||
<FlashList
|
|
||||||
key={seasonIndex}
|
|
||||||
data={groupBySeason}
|
|
||||||
renderItem={({ item }) => <EpisodeCard item={item} />}
|
|
||||||
keyExtractor={(item, index) => item.Id ?? `episode-${index}`}
|
|
||||||
ListHeaderComponent={ListHeaderComponent}
|
|
||||||
contentInsetAdjustmentBehavior='automatic'
|
|
||||||
contentContainerStyle={{
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingLeft: insets.left + 16,
|
|
||||||
paddingRight: insets.right + 16,
|
|
||||||
paddingTop: Platform.OS === "android" ? 10 : 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BottomSheetModal } from "@gorhom/bottom-sheet";
|
import { BottomSheetModal } from "@gorhom/bottom-sheet";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -13,8 +13,10 @@ import ActiveDownloads from "@/components/downloads/ActiveDownloads";
|
|||||||
import { DownloadSize } from "@/components/downloads/DownloadSize";
|
import { DownloadSize } from "@/components/downloads/DownloadSize";
|
||||||
import { MovieCard } from "@/components/downloads/MovieCard";
|
import { MovieCard } from "@/components/downloads/MovieCard";
|
||||||
import { SeriesCard } from "@/components/downloads/SeriesCard";
|
import { SeriesCard } from "@/components/downloads/SeriesCard";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { type DownloadedItem } from "@/providers/Downloads/types";
|
import { type DownloadedItem } from "@/providers/Downloads/types";
|
||||||
|
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
||||||
import { queueAtom } from "@/utils/atoms/queue";
|
import { queueAtom } from "@/utils/atoms/queue";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
|
|
||||||
@@ -161,145 +163,99 @@ export default function page() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<OfflineModeProvider isOffline={true}>
|
||||||
showsVerticalScrollIndicator={false}
|
<ScrollView
|
||||||
contentInsetAdjustmentBehavior='automatic'
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
<View style={{ paddingTop: Platform.OS === "android" ? 17 : 0 }}>
|
>
|
||||||
<View className='mb-4 flex flex-col space-y-4 px-4'>
|
<View style={{ paddingTop: Platform.OS === "android" ? 17 : 0 }}>
|
||||||
{/* Queue card - hidden */}
|
<View className='mb-4 flex flex-col space-y-4 px-4'>
|
||||||
{/* <View className='bg-neutral-900 p-4 rounded-2xl'>
|
<ActiveDownloads />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{movies.length > 0 && (
|
||||||
|
<View className='mb-4'>
|
||||||
|
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
||||||
<Text className='text-lg font-bold'>
|
<Text className='text-lg font-bold'>
|
||||||
{t("home.downloads.queue")}
|
{t("home.downloads.movies")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className='text-xs opacity-70 text-red-600'>
|
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
||||||
{t("home.downloads.queue_hint")}
|
<Text className='text-xs font-bold'>{movies?.length}</Text>
|
||||||
</Text>
|
</View>
|
||||||
<View className='flex flex-col space-y-2 mt-2'>
|
</View>
|
||||||
{queue.map((q, index) => (
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
<TouchableOpacity
|
<View className='px-4 flex flex-row'>
|
||||||
onPress={() =>
|
{movies?.map((item) => (
|
||||||
router.push(`/(auth)/items/page?id=${q.item.Id}`)
|
<TouchableItemRouter item={item.item} key={item.item.Id}>
|
||||||
}
|
<MovieCard item={item.item} />
|
||||||
className='relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between'
|
</TouchableItemRouter>
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<View>
|
|
||||||
<Text className='font-semibold'>{q.item.Name}</Text>
|
|
||||||
<Text className='text-xs opacity-50'>
|
|
||||||
{q.item.Type}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
removeProcess(q.id);
|
|
||||||
setQueue((prev) => {
|
|
||||||
if (!prev) return [];
|
|
||||||
return [...prev.filter((i) => i.id !== q.id)];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name='close' size={24} color='red' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
{queue.length === 0 && (
|
|
||||||
<Text className='opacity-50'>
|
|
||||||
{t("home.downloads.no_items_in_queue")}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View> */}
|
|
||||||
|
|
||||||
<ActiveDownloads />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{movies.length > 0 && (
|
|
||||||
<View className='mb-4'>
|
|
||||||
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
|
||||||
<Text className='text-lg font-bold'>
|
|
||||||
{t("home.downloads.movies")}
|
|
||||||
</Text>
|
|
||||||
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
|
||||||
<Text className='text-xs font-bold'>{movies?.length}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
)}
|
||||||
<View className='px-4 flex flex-row'>
|
{groupedBySeries.length > 0 && (
|
||||||
{movies?.map((item) => (
|
<View className='mb-4'>
|
||||||
<TouchableItemRouter
|
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
||||||
item={item.item}
|
<Text className='text-lg font-bold'>
|
||||||
isOffline
|
{t("home.downloads.tvseries")}
|
||||||
key={item.item.Id}
|
|
||||||
>
|
|
||||||
<MovieCard item={item.item} />
|
|
||||||
</TouchableItemRouter>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{groupedBySeries.length > 0 && (
|
|
||||||
<View className='mb-4'>
|
|
||||||
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
|
||||||
<Text className='text-lg font-bold'>
|
|
||||||
{t("home.downloads.tvseries")}
|
|
||||||
</Text>
|
|
||||||
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
|
||||||
<Text className='text-xs font-bold'>
|
|
||||||
{groupedBySeries?.length}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
||||||
|
<Text className='text-xs font-bold'>
|
||||||
|
{groupedBySeries?.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
<View className='px-4 flex flex-row'>
|
||||||
<View className='px-4 flex flex-row'>
|
{groupedBySeries?.map((items) => (
|
||||||
{groupedBySeries?.map((items) => (
|
<View
|
||||||
<View className='mb-2 last:mb-0' key={items[0].item.SeriesId}>
|
className='mb-2 last:mb-0'
|
||||||
<SeriesCard
|
|
||||||
items={items.map((i) => i.item)}
|
|
||||||
key={items[0].item.SeriesId}
|
key={items[0].item.SeriesId}
|
||||||
/>
|
>
|
||||||
</View>
|
<SeriesCard
|
||||||
))}
|
items={items.map((i) => i.item)}
|
||||||
</View>
|
key={items[0].item.SeriesId}
|
||||||
</ScrollView>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
))}
|
||||||
|
</View>
|
||||||
{otherMedia.length > 0 && (
|
</ScrollView>
|
||||||
<View className='mb-4'>
|
|
||||||
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
|
||||||
<Text className='text-lg font-bold'>
|
|
||||||
{t("home.downloads.other_media")}
|
|
||||||
</Text>
|
|
||||||
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
|
||||||
<Text className='text-xs font-bold'>{otherMedia?.length}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
)}
|
||||||
<View className='px-4 flex flex-row'>
|
|
||||||
{otherMedia?.map((item) => (
|
{otherMedia.length > 0 && (
|
||||||
<TouchableItemRouter
|
<View className='mb-4'>
|
||||||
item={item.item}
|
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
||||||
isOffline
|
<Text className='text-lg font-bold'>
|
||||||
key={item.item.Id}
|
{t("home.downloads.other_media")}
|
||||||
>
|
</Text>
|
||||||
<MovieCard item={item.item} />
|
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
||||||
</TouchableItemRouter>
|
<Text className='text-xs font-bold'>
|
||||||
))}
|
{otherMedia?.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
</View>
|
<View className='px-4 flex flex-row'>
|
||||||
)}
|
{otherMedia?.map((item) => (
|
||||||
{downloadedFiles?.length === 0 && (
|
<TouchableItemRouter item={item.item} key={item.item.Id}>
|
||||||
<View className='flex px-4'>
|
<MovieCard item={item.item} />
|
||||||
<Text className='opacity-50'>
|
</TouchableItemRouter>
|
||||||
{t("home.downloads.no_downloaded_items")}
|
))}
|
||||||
</Text>
|
</View>
|
||||||
</View>
|
</ScrollView>
|
||||||
)}
|
</View>
|
||||||
</View>
|
)}
|
||||||
</ScrollView>
|
{downloadedFiles?.length === 0 && (
|
||||||
|
<View className='flex px-4'>
|
||||||
|
<Text className='opacity-50'>
|
||||||
|
{t("home.downloads.no_downloaded_items")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</OfflineModeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -11,6 +11,7 @@ import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
|||||||
import { QuickConnect } from "@/components/settings/QuickConnect";
|
import { QuickConnect } from "@/components/settings/QuickConnect";
|
||||||
import { StorageSettings } from "@/components/settings/StorageSettings";
|
import { StorageSettings } from "@/components/settings/StorageSettings";
|
||||||
import { UserInfo } from "@/components/settings/UserInfo";
|
import { UserInfo } from "@/components/settings/UserInfo";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
import { useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
|
||||||
export default function settings() {
|
export default function settings() {
|
||||||
@@ -90,6 +91,11 @@ export default function settings() {
|
|||||||
showArrow
|
showArrow
|
||||||
title={t("home.settings.intro.title")}
|
title={t("home.settings.intro.title")}
|
||||||
/>
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/network/page")}
|
||||||
|
showArrow
|
||||||
|
title={t("home.settings.network.title")}
|
||||||
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/logs/page")}
|
onPress={() => router.push("/settings/logs/page")}
|
||||||
showArrow
|
showArrow
|
||||||
|
|||||||
48
app/(auth)/(tabs)/(home)/settings/network/page.tsx
Normal file
48
app/(auth)/(tabs)/(home)/settings/network/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Platform, ScrollView, View } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { ListGroup } from "@/components/list/ListGroup";
|
||||||
|
import { ListItem } from "@/components/list/ListItem";
|
||||||
|
import { LocalNetworkSettings } from "@/components/settings/LocalNetworkSettings";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
|
||||||
|
export default function NetworkSettingsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
|
|
||||||
|
const remoteUrl = storage.getString("serverUrl");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior='automatic'
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: insets.left,
|
||||||
|
paddingRight: insets.right,
|
||||||
|
paddingBottom: insets.bottom + 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className='p-4 flex flex-col'
|
||||||
|
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
|
||||||
|
>
|
||||||
|
<ListGroup title={t("home.settings.network.current_server")}>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.network.remote_url")}
|
||||||
|
subtitle={remoteUrl ?? t("home.settings.network.not_configured")}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.network.active_url")}
|
||||||
|
subtitle={api?.basePath ?? t("home.settings.network.not_connected")}
|
||||||
|
/>
|
||||||
|
</ListGroup>
|
||||||
|
|
||||||
|
<View className='mt-4'>
|
||||||
|
<LocalNetworkSettings />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import Animated, {
|
|||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { ItemContent } from "@/components/ItemContent";
|
import { ItemContent } from "@/components/ItemContent";
|
||||||
import { useItemQuery } from "@/hooks/useItemQuery";
|
import { useItemQuery } from "@/hooks/useItemQuery";
|
||||||
|
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
const { id } = useLocalSearchParams() as { id: string };
|
const { id } = useLocalSearchParams() as { id: string };
|
||||||
@@ -75,39 +76,35 @@ const Page: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='flex flex-1 relative'>
|
<OfflineModeProvider isOffline={isOffline}>
|
||||||
<Animated.View
|
<View className='flex flex-1 relative'>
|
||||||
pointerEvents={"none"}
|
<Animated.View
|
||||||
style={[animatedStyle]}
|
pointerEvents={"none"}
|
||||||
className='absolute top-0 left-0 flex flex-col items-start h-screen w-screen px-4 z-50 bg-black'
|
style={[animatedStyle]}
|
||||||
>
|
className='absolute top-0 left-0 flex flex-col items-start h-screen w-screen px-4 z-50 bg-black'
|
||||||
<View
|
>
|
||||||
style={{
|
<View
|
||||||
height: item?.Type === "Episode" ? 300 : 450,
|
style={{
|
||||||
}}
|
height: item?.Type === "Episode" ? 300 : 450,
|
||||||
className='bg-transparent rounded-lg mb-4 w-full'
|
}}
|
||||||
/>
|
className='bg-transparent rounded-lg mb-4 w-full'
|
||||||
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
|
/>
|
||||||
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
|
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
|
||||||
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
|
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
|
||||||
<View className='flex flex-row space-x-1 mb-8'>
|
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
|
||||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
<View className='flex flex-row space-x-1 mb-8'>
|
||||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||||
</View>
|
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||||
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
|
</View>
|
||||||
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2' />
|
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
|
||||||
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
|
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2' />
|
||||||
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
|
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
|
||||||
</Animated.View>
|
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
|
||||||
{item && (
|
</Animated.View>
|
||||||
<ItemContent
|
{item && <ItemContent item={item} itemWithSources={itemWithSources} />}
|
||||||
item={item}
|
</View>
|
||||||
isOffline={isOffline}
|
</OfflineModeProvider>
|
||||||
itemWithSources={itemWithSources}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -27,6 +27,7 @@ import { PlatformDropdown } from "@/components/PlatformDropdown";
|
|||||||
import { JellyserrRatings } from "@/components/Ratings";
|
import { JellyserrRatings } from "@/components/Ratings";
|
||||||
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
|
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
|
||||||
import { ItemActions } from "@/components/series/SeriesActions";
|
import { ItemActions } from "@/components/series/SeriesActions";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
||||||
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
|
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
|
||||||
|
|||||||
@@ -14,86 +14,124 @@ import { ParallaxScrollView } from "@/components/ParallaxPage";
|
|||||||
import { NextUp } from "@/components/series/NextUp";
|
import { NextUp } from "@/components/series/NextUp";
|
||||||
import { SeasonPicker } from "@/components/series/SeasonPicker";
|
import { SeasonPicker } from "@/components/series/SeasonPicker";
|
||||||
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
||||||
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
||||||
|
import {
|
||||||
|
buildOfflineSeriesFromEpisodes,
|
||||||
|
getDownloadedEpisodesForSeries,
|
||||||
|
} from "@/utils/downloads/offline-series";
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||||
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
|
||||||
const page: React.FC = () => {
|
const page: React.FC = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const { id: seriesId, seasonIndex } = params as {
|
const {
|
||||||
|
id: seriesId,
|
||||||
|
seasonIndex,
|
||||||
|
offline: offlineParam,
|
||||||
|
} = params as {
|
||||||
id: string;
|
id: string;
|
||||||
seasonIndex: string;
|
seasonIndex: string;
|
||||||
|
offline?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOffline = offlineParam === "true";
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
const { getDownloadedItems, downloadedItems } = useDownload();
|
||||||
|
|
||||||
|
// For offline mode, construct series data from downloaded episodes
|
||||||
|
// Include downloadedItems.length so query refetches when items are deleted
|
||||||
const { data: item } = useQuery({
|
const { data: item } = useQuery({
|
||||||
queryKey: ["series", seriesId],
|
queryKey: ["series", seriesId, isOffline, downloadedItems.length],
|
||||||
queryFn: async () =>
|
queryFn: async () => {
|
||||||
await getUserItemData({
|
if (isOffline) {
|
||||||
|
return buildOfflineSeriesFromEpisodes(getDownloadedItems(), seriesId);
|
||||||
|
}
|
||||||
|
return await getUserItemData({
|
||||||
api,
|
api,
|
||||||
userId: user?.Id,
|
userId: user?.Id,
|
||||||
itemId: seriesId,
|
itemId: seriesId,
|
||||||
}),
|
});
|
||||||
staleTime: 60 * 1000,
|
},
|
||||||
|
staleTime: isOffline ? Infinity : 60 * 1000,
|
||||||
|
enabled: isOffline || (!!api && !!user?.Id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const backdropUrl = useMemo(
|
// For offline mode, use stored base64 image
|
||||||
() =>
|
const base64Image = useMemo(() => {
|
||||||
getBackdropUrl({
|
if (isOffline) {
|
||||||
api,
|
return storage.getString(seriesId);
|
||||||
item,
|
}
|
||||||
quality: 90,
|
return null;
|
||||||
width: 1000,
|
}, [isOffline, seriesId]);
|
||||||
}),
|
|
||||||
[item],
|
|
||||||
);
|
|
||||||
|
|
||||||
const logoUrl = useMemo(
|
const backdropUrl = useMemo(() => {
|
||||||
() =>
|
if (isOffline && base64Image) {
|
||||||
getLogoImageUrlById({
|
return `data:image/jpeg;base64,${base64Image}`;
|
||||||
api,
|
}
|
||||||
item,
|
return getBackdropUrl({
|
||||||
}),
|
api,
|
||||||
[item],
|
item,
|
||||||
);
|
quality: 90,
|
||||||
|
width: 1000,
|
||||||
|
});
|
||||||
|
}, [isOffline, base64Image, api, item]);
|
||||||
|
|
||||||
|
const logoUrl = useMemo(() => {
|
||||||
|
if (isOffline) {
|
||||||
|
return null; // No logo in offline mode
|
||||||
|
}
|
||||||
|
return getLogoImageUrlById({
|
||||||
|
api,
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
}, [isOffline, api, item]);
|
||||||
|
|
||||||
const { data: allEpisodes, isLoading } = useQuery({
|
const { data: allEpisodes, isLoading } = useQuery({
|
||||||
queryKey: ["AllEpisodes", item?.Id],
|
queryKey: ["AllEpisodes", seriesId, isOffline, downloadedItems.length],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!api || !user?.Id || !item?.Id) return [];
|
if (isOffline) {
|
||||||
|
return getDownloadedEpisodesForSeries(getDownloadedItems(), seriesId);
|
||||||
|
}
|
||||||
|
if (!api || !user?.Id) return [];
|
||||||
|
|
||||||
const res = await getTvShowsApi(api).getEpisodes({
|
const res = await getTvShowsApi(api).getEpisodes({
|
||||||
seriesId: item.Id,
|
seriesId: seriesId,
|
||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
enableUserData: true,
|
enableUserData: true,
|
||||||
// Note: Including trick play is necessary to enable trick play downloads
|
|
||||||
fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
|
fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
|
||||||
});
|
});
|
||||||
return res?.data.Items || [];
|
return res?.data.Items || [];
|
||||||
},
|
},
|
||||||
select: (data) =>
|
select: (data) =>
|
||||||
// This needs to be sorted by parent index number and then index number, that way we can download the episodes in the correct order.
|
|
||||||
[...(data || [])].sort(
|
[...(data || [])].sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.ParentIndexNumber ?? 0) - (b.ParentIndexNumber ?? 0) ||
|
(a.ParentIndexNumber ?? 0) - (b.ParentIndexNumber ?? 0) ||
|
||||||
(a.IndexNumber ?? 0) - (b.IndexNumber ?? 0),
|
(a.IndexNumber ?? 0) - (b.IndexNumber ?? 0),
|
||||||
),
|
),
|
||||||
staleTime: 60,
|
staleTime: isOffline ? Infinity : 60,
|
||||||
enabled: !!api && !!user?.Id && !!item?.Id,
|
enabled: isOffline || (!!api && !!user?.Id),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Don't show header buttons in offline mode
|
||||||
|
if (isOffline) {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerRight: () => null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerRight: () =>
|
headerRight: () =>
|
||||||
!isLoading &&
|
!isLoading && item && allEpisodes && allEpisodes.length > 0 ? (
|
||||||
item &&
|
|
||||||
allEpisodes &&
|
|
||||||
allEpisodes.length > 0 && (
|
|
||||||
<View className='flex flex-row items-center space-x-2'>
|
<View className='flex flex-row items-center space-x-2'>
|
||||||
<AddToFavorites item={item} />
|
<AddToFavorites item={item} />
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
@@ -114,49 +152,64 @@ const page: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
),
|
) : null,
|
||||||
});
|
});
|
||||||
}, [allEpisodes, isLoading, item]);
|
}, [allEpisodes, isLoading, item, isOffline]);
|
||||||
|
|
||||||
if (!item || !backdropUrl) return null;
|
// For offline mode, we can show the page even without backdropUrl
|
||||||
|
if (!item || (!isOffline && !backdropUrl)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParallaxScrollView
|
<OfflineModeProvider isOffline={isOffline}>
|
||||||
headerHeight={400}
|
<ParallaxScrollView
|
||||||
headerImage={
|
headerHeight={400}
|
||||||
<Image
|
headerImage={
|
||||||
source={{
|
backdropUrl ? (
|
||||||
uri: backdropUrl,
|
<Image
|
||||||
}}
|
source={{
|
||||||
style={{
|
uri: backdropUrl,
|
||||||
width: "100%",
|
}}
|
||||||
height: "100%",
|
style={{
|
||||||
}}
|
width: "100%",
|
||||||
/>
|
height: "100%",
|
||||||
}
|
}}
|
||||||
logo={
|
/>
|
||||||
logoUrl ? (
|
) : (
|
||||||
<Image
|
<View
|
||||||
source={{
|
style={{
|
||||||
uri: logoUrl,
|
width: "100%",
|
||||||
}}
|
height: "100%",
|
||||||
style={{
|
backgroundColor: "#1a1a1a",
|
||||||
height: 130,
|
}}
|
||||||
width: "100%",
|
/>
|
||||||
}}
|
)
|
||||||
contentFit='contain'
|
}
|
||||||
/>
|
logo={
|
||||||
) : undefined
|
logoUrl ? (
|
||||||
}
|
<Image
|
||||||
>
|
source={{
|
||||||
<View className='flex flex-col pt-4'>
|
uri: logoUrl,
|
||||||
<SeriesHeader item={item} />
|
}}
|
||||||
<View className='mb-4'>
|
style={{
|
||||||
<NextUp seriesId={seriesId} />
|
height: 130,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
contentFit='contain'
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View className='flex flex-col pt-4'>
|
||||||
|
<SeriesHeader item={item} />
|
||||||
|
{!isOffline && (
|
||||||
|
<View className='mb-4'>
|
||||||
|
<NextUp seriesId={seriesId} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<SeasonPicker item={item} initialSeasonIndex={Number(seasonIndex)} />
|
||||||
</View>
|
</View>
|
||||||
<SeasonPicker item={item} initialSeasonIndex={Number(seasonIndex)} />
|
</ParallaxScrollView>
|
||||||
</View>
|
</OfflineModeProvider>
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,10 @@ const Page = () => {
|
|||||||
itemType = "Series";
|
itemType = "Series";
|
||||||
} else if (library.CollectionType === "boxsets") {
|
} else if (library.CollectionType === "boxsets") {
|
||||||
itemType = "BoxSet";
|
itemType = "BoxSet";
|
||||||
|
} else if (library.CollectionType === "homevideos") {
|
||||||
|
itemType = "Video";
|
||||||
|
} else if (library.CollectionType === "musicvideos") {
|
||||||
|
itemType = "MusicVideo";
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getItemsApi(api).getItems({
|
const response = await getItemsApi(api).getItems({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useAsyncDebouncer } from "@tanstack/react-pacer";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -36,6 +36,7 @@ import { DiscoverFilters } from "@/components/search/DiscoverFilters";
|
|||||||
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
|
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
|
||||||
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
|
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
|
||||||
import { SearchTabButtons } from "@/components/search/SearchTabButtons";
|
import { SearchTabButtons } from "@/components/search/SearchTabButtons";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
@@ -57,6 +58,7 @@ const exampleSearches = [
|
|||||||
export default function search() {
|
export default function search() {
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import { useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -19,6 +19,7 @@ import { Text } from "@/components/common/Text";
|
|||||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||||
import { ItemCardText } from "@/components/ItemCardText";
|
import { ItemCardText } from "@/components/ItemCardText";
|
||||||
import { ItemPoster } from "@/components/posters/ItemPoster";
|
import { ItemPoster } from "@/components/posters/ItemPoster";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import {
|
import {
|
||||||
useDeleteWatchlist,
|
useDeleteWatchlist,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { Pressable } from "react-native-gesture-handler";
|
import { Pressable } from "react-native-gesture-handler";
|
||||||
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useStreamystatsEnabled } from "@/hooks/useWatchlists";
|
import { useStreamystatsEnabled } from "@/hooks/useWatchlists";
|
||||||
|
|
||||||
export default function WatchlistsLayout() {
|
export default function WatchlistsLayout() {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useCreateWatchlist } from "@/hooks/useWatchlistMutations";
|
import { useCreateWatchlist } from "@/hooks/useWatchlistMutations";
|
||||||
import type {
|
import type {
|
||||||
StreamystatsWatchlistAllowedItemType,
|
StreamystatsWatchlistAllowedItemType,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useUpdateWatchlist } from "@/hooks/useWatchlistMutations";
|
import { useUpdateWatchlist } from "@/hooks/useWatchlistMutations";
|
||||||
import { useWatchlistDetailQuery } from "@/hooks/useWatchlists";
|
import { useWatchlistDetailQuery } from "@/hooks/useWatchlists";
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -8,6 +7,7 @@ import { Platform, RefreshControl, TouchableOpacity, View } from "react-native";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import {
|
import {
|
||||||
useStreamystatsEnabled,
|
useStreamystatsEnabled,
|
||||||
useWatchlistsQuery,
|
useWatchlistsQuery,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type {
|
|||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -38,6 +37,7 @@ import { Text } from "@/components/common/Text";
|
|||||||
import { CreatePlaylistModal } from "@/components/music/CreatePlaylistModal";
|
import { CreatePlaylistModal } from "@/components/music/CreatePlaylistModal";
|
||||||
import { PlaylistPickerSheet } from "@/components/music/PlaylistPickerSheet";
|
import { PlaylistPickerSheet } from "@/components/music/PlaylistPickerSheet";
|
||||||
import { TrackOptionsSheet } from "@/components/music/TrackOptionsSheet";
|
import { TrackOptionsSheet } from "@/components/music/TrackOptionsSheet";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useFavorite } from "@/hooks/useFavorite";
|
import { useFavorite } from "@/hooks/useFavorite";
|
||||||
import { useMusicCast } from "@/hooks/useMusicCast";
|
import { useMusicCast } from "@/hooks/useMusicCast";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ import {
|
|||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
|
||||||
import { router, useGlobalSearchParams, useNavigation } from "expo-router";
|
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert, Platform, useWindowDimensions, View } from "react-native";
|
import { Alert, Platform, useWindowDimensions, View } from "react-native";
|
||||||
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
|
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
|
||||||
|
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
PlaybackSpeedScope,
|
PlaybackSpeedScope,
|
||||||
updatePlaybackSpeedSettings,
|
updatePlaybackSpeedSettings,
|
||||||
} from "@/components/video-player/controls/utils/playback-speed-settings";
|
} from "@/components/video-player/controls/utils/playback-speed-settings";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||||
@@ -44,6 +44,9 @@ import {
|
|||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { DownloadedItem } from "@/providers/Downloads/types";
|
import { DownloadedItem } from "@/providers/Downloads/types";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
|
||||||
|
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
|
||||||
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import {
|
import {
|
||||||
@@ -60,6 +63,7 @@ export default function page() {
|
|||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const router = useRouter();
|
||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
|
|
||||||
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
||||||
@@ -78,6 +82,7 @@ export default function page() {
|
|||||||
const [tracksReady, setTracksReady] = useState(false);
|
const [tracksReady, setTracksReady] = useState(false);
|
||||||
const [hasPlaybackStarted, setHasPlaybackStarted] = useState(false);
|
const [hasPlaybackStarted, setHasPlaybackStarted] = useState(false);
|
||||||
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1.0);
|
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1.0);
|
||||||
|
const [showTechnicalInfo, setShowTechnicalInfo] = useState(false);
|
||||||
|
|
||||||
const progress = useSharedValue(0);
|
const progress = useSharedValue(0);
|
||||||
const isSeeking = useSharedValue(false);
|
const isSeeking = useSharedValue(false);
|
||||||
@@ -87,10 +92,9 @@ export default function page() {
|
|||||||
: require("react-native-volume-manager");
|
: require("react-native-volume-manager");
|
||||||
|
|
||||||
const downloadUtils = useDownload();
|
const downloadUtils = useDownload();
|
||||||
const downloadedFiles = useMemo(
|
// Call directly instead of useMemo - the function reference doesn't change
|
||||||
() => downloadUtils.getDownloadedItems(),
|
// when data updates, only when the provider initializes
|
||||||
[downloadUtils.getDownloadedItems],
|
const downloadedFiles = downloadUtils.getDownloadedItems();
|
||||||
);
|
|
||||||
|
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||||
|
|
||||||
@@ -109,7 +113,7 @@ export default function page() {
|
|||||||
bitrateValue: bitrateValueStr,
|
bitrateValue: bitrateValueStr,
|
||||||
offline: offlineStr,
|
offline: offlineStr,
|
||||||
playbackPosition: playbackPositionFromUrl,
|
playbackPosition: playbackPositionFromUrl,
|
||||||
} = useGlobalSearchParams<{
|
} = useLocalSearchParams<{
|
||||||
itemId: string;
|
itemId: string;
|
||||||
audioIndex: string;
|
audioIndex: string;
|
||||||
subtitleIndex: string;
|
subtitleIndex: string;
|
||||||
@@ -527,7 +531,11 @@ export default function page() {
|
|||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
isTranscoding,
|
isTranscoding,
|
||||||
);
|
);
|
||||||
const initialAudioId = getMpvAudioId(mediaSource, audioIndex);
|
const initialAudioId = getMpvAudioId(
|
||||||
|
mediaSource,
|
||||||
|
audioIndex,
|
||||||
|
isTranscoding,
|
||||||
|
);
|
||||||
|
|
||||||
// Calculate start position directly here to avoid timing issues
|
// Calculate start position directly here to avoid timing issues
|
||||||
const startTicks = playbackPositionFromUrl
|
const startTicks = playbackPositionFromUrl
|
||||||
@@ -677,8 +685,8 @@ export default function page() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading !== undefined) {
|
||||||
setIsBuffering(true);
|
setIsBuffering(isLoading);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[playbackManager, item?.Id, progress],
|
[playbackManager, item?.Id, progress],
|
||||||
@@ -723,6 +731,59 @@ export default function page() {
|
|||||||
videoRef.current?.seekTo?.(position / 1000);
|
videoRef.current?.seekTo?.(position / 1000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Technical info toggle handler
|
||||||
|
const handleToggleTechnicalInfo = useCallback(() => {
|
||||||
|
setShowTechnicalInfo((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get technical info from the player
|
||||||
|
const getTechnicalInfo = useCallback(async () => {
|
||||||
|
return (await videoRef.current?.getTechnicalInfo?.()) ?? {};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Determine play method based on stream URL and media source
|
||||||
|
const playMethod = useMemo<
|
||||||
|
"DirectPlay" | "DirectStream" | "Transcode" | undefined
|
||||||
|
>(() => {
|
||||||
|
if (!stream?.url) return undefined;
|
||||||
|
|
||||||
|
// Check if transcoding (m3u8 playlist or TranscodingUrl present)
|
||||||
|
if (stream.url.includes("m3u8") || stream.mediaSource?.TranscodingUrl) {
|
||||||
|
return "Transcode";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if direct play (no container remuxing needed)
|
||||||
|
// Direct play means the file is being served as-is
|
||||||
|
if (stream.url.includes("/Videos/") && stream.url.includes("/stream")) {
|
||||||
|
return "DirectStream";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to direct play if we're not transcoding
|
||||||
|
return "DirectPlay";
|
||||||
|
}, [stream?.url, stream?.mediaSource?.TranscodingUrl]);
|
||||||
|
|
||||||
|
// Extract transcode reasons from the TranscodingUrl
|
||||||
|
const transcodeReasons = useMemo<string[]>(() => {
|
||||||
|
const transcodingUrl = stream?.mediaSource?.TranscodingUrl;
|
||||||
|
if (!transcodingUrl) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the TranscodeReasons parameter from the URL
|
||||||
|
const url = new URL(transcodingUrl, "http://localhost");
|
||||||
|
const reasons = url.searchParams.get("TranscodeReasons");
|
||||||
|
if (reasons) {
|
||||||
|
return reasons.split(",").filter(Boolean);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If URL parsing fails, try regex fallback
|
||||||
|
const match = transcodingUrl.match(/TranscodeReasons=([^&]+)/);
|
||||||
|
if (match) {
|
||||||
|
return match[1].split(",").filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [stream?.mediaSource?.TranscodingUrl]);
|
||||||
|
|
||||||
const handleZoomToggle = useCallback(async () => {
|
const handleZoomToggle = useCallback(async () => {
|
||||||
const newZoomState = !isZoomedToFill;
|
const newZoomState = !isZoomedToFill;
|
||||||
await videoRef.current?.setZoomedToFill?.(newZoomState);
|
await videoRef.current?.setZoomedToFill?.(newZoomState);
|
||||||
@@ -833,99 +894,104 @@ export default function page() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlayerProvider
|
<OfflineModeProvider isOffline={offline}>
|
||||||
playerRef={videoRef}
|
<PlayerProvider
|
||||||
item={item}
|
playerRef={videoRef}
|
||||||
mediaSource={stream?.mediaSource}
|
item={item}
|
||||||
isVideoLoaded={isVideoLoaded}
|
mediaSource={stream?.mediaSource}
|
||||||
tracksReady={tracksReady}
|
isVideoLoaded={isVideoLoaded}
|
||||||
offline={offline}
|
tracksReady={tracksReady}
|
||||||
downloadedItem={downloadedItem}
|
downloadedItem={downloadedItem}
|
||||||
>
|
>
|
||||||
<VideoProvider>
|
<VideoProvider>
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "black",
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
flex: 1,
|
||||||
width: "100%",
|
backgroundColor: "black",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
position: "relative",
|
width: "100%",
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MpvPlayerView
|
<View
|
||||||
ref={videoRef}
|
style={{
|
||||||
source={videoSource}
|
display: "flex",
|
||||||
style={{ width: "100%", height: "100%" }}
|
width: "100%",
|
||||||
onProgress={onProgress}
|
height: "100%",
|
||||||
onPlaybackStateChange={onPlaybackStateChanged}
|
position: "relative",
|
||||||
onLoad={() => setIsVideoLoaded(true)}
|
flexDirection: "column",
|
||||||
onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => {
|
justifyContent: "center",
|
||||||
console.error("Video Error:", e.nativeEvent);
|
|
||||||
Alert.alert(
|
|
||||||
t("player.error"),
|
|
||||||
t("player.an_error_occured_while_playing_the_video"),
|
|
||||||
);
|
|
||||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
|
||||||
}}
|
}}
|
||||||
onTracksReady={() => {
|
>
|
||||||
setTracksReady(true);
|
<MpvPlayerView
|
||||||
}}
|
ref={videoRef}
|
||||||
/>
|
source={videoSource}
|
||||||
{!hasPlaybackStarted && (
|
style={{ width: "100%", height: "100%" }}
|
||||||
<View
|
onProgress={onProgress}
|
||||||
style={{
|
onPlaybackStateChange={onPlaybackStateChanged}
|
||||||
position: "absolute",
|
onLoad={() => setIsVideoLoaded(true)}
|
||||||
top: 0,
|
onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => {
|
||||||
left: 0,
|
console.error("Video Error:", e.nativeEvent);
|
||||||
right: 0,
|
Alert.alert(
|
||||||
bottom: 0,
|
t("player.error"),
|
||||||
backgroundColor: "black",
|
t("player.an_error_occured_while_playing_the_video"),
|
||||||
justifyContent: "center",
|
);
|
||||||
alignItems: "center",
|
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||||
}}
|
}}
|
||||||
>
|
onTracksReady={() => {
|
||||||
<Loader />
|
setTracksReady(true);
|
||||||
</View>
|
}}
|
||||||
|
/>
|
||||||
|
{!hasPlaybackStarted && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "black",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Loader />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{isMounted === true && item && !isPipMode && (
|
||||||
|
<Controls
|
||||||
|
mediaSource={stream?.mediaSource}
|
||||||
|
item={item}
|
||||||
|
togglePlay={togglePlay}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
isSeeking={isSeeking}
|
||||||
|
progress={progress}
|
||||||
|
cacheProgress={cacheProgress}
|
||||||
|
isBuffering={isBuffering}
|
||||||
|
showControls={showControls}
|
||||||
|
setShowControls={setShowControls}
|
||||||
|
startPictureInPicture={startPictureInPicture}
|
||||||
|
play={play}
|
||||||
|
pause={pause}
|
||||||
|
seek={seek}
|
||||||
|
enableTrickplay={true}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
isZoomedToFill={isZoomedToFill}
|
||||||
|
onZoomToggle={handleZoomToggle}
|
||||||
|
api={api}
|
||||||
|
downloadedFiles={downloadedFiles}
|
||||||
|
playbackSpeed={currentPlaybackSpeed}
|
||||||
|
setPlaybackSpeed={handleSetPlaybackSpeed}
|
||||||
|
showTechnicalInfo={showTechnicalInfo}
|
||||||
|
onToggleTechnicalInfo={handleToggleTechnicalInfo}
|
||||||
|
getTechnicalInfo={getTechnicalInfo}
|
||||||
|
playMethod={playMethod}
|
||||||
|
transcodeReasons={transcodeReasons}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{isMounted === true && item && !isPipMode && (
|
</VideoProvider>
|
||||||
<Controls
|
</PlayerProvider>
|
||||||
mediaSource={stream?.mediaSource}
|
</OfflineModeProvider>
|
||||||
item={item}
|
|
||||||
togglePlay={togglePlay}
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
isSeeking={isSeeking}
|
|
||||||
progress={progress}
|
|
||||||
cacheProgress={cacheProgress}
|
|
||||||
isBuffering={isBuffering}
|
|
||||||
showControls={showControls}
|
|
||||||
setShowControls={setShowControls}
|
|
||||||
startPictureInPicture={startPictureInPicture}
|
|
||||||
play={play}
|
|
||||||
pause={pause}
|
|
||||||
seek={seek}
|
|
||||||
enableTrickplay={true}
|
|
||||||
offline={offline}
|
|
||||||
aspectRatio={aspectRatio}
|
|
||||||
isZoomedToFill={isZoomedToFill}
|
|
||||||
onZoomToggle={handleZoomToggle}
|
|
||||||
api={api}
|
|
||||||
downloadedFiles={downloadedFiles}
|
|
||||||
playbackSpeed={currentPlaybackSpeed}
|
|
||||||
setPlaybackSpeed={handleSetPlaybackSpeed}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</VideoProvider>
|
|
||||||
</PlayerProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
164
app/_layout.tsx
164
app/_layout.tsx
@@ -22,6 +22,7 @@ import {
|
|||||||
import { MusicPlayerProvider } from "@/providers/MusicPlayerProvider";
|
import { MusicPlayerProvider } from "@/providers/MusicPlayerProvider";
|
||||||
import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider";
|
import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider";
|
||||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||||
|
import { ServerUrlProvider } from "@/providers/ServerUrlProvider";
|
||||||
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import {
|
import {
|
||||||
@@ -47,7 +48,7 @@ import type {
|
|||||||
NotificationResponse,
|
NotificationResponse,
|
||||||
} from "expo-notifications/build/Notifications.types";
|
} from "expo-notifications/build/Notifications.types";
|
||||||
import type { ExpoPushToken } from "expo-notifications/build/Tokens.types";
|
import type { ExpoPushToken } from "expo-notifications/build/Tokens.types";
|
||||||
import { router, Stack, useSegments } from "expo-router";
|
import { Stack, useSegments } from "expo-router";
|
||||||
import * as SplashScreen from "expo-splash-screen";
|
import * as SplashScreen from "expo-splash-screen";
|
||||||
import * as TaskManager from "expo-task-manager";
|
import * as TaskManager from "expo-task-manager";
|
||||||
import { Provider as JotaiProvider, useAtom } from "jotai";
|
import { Provider as JotaiProvider, useAtom } from "jotai";
|
||||||
@@ -56,6 +57,7 @@ import { I18nextProvider } from "react-i18next";
|
|||||||
import { Appearance } from "react-native";
|
import { Appearance } from "react-native";
|
||||||
import { SystemBars } from "react-native-edge-to-edge";
|
import { SystemBars } from "react-native-edge-to-edge";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { userAtom } from "@/providers/JellyfinProvider";
|
import { userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { store } from "@/utils/store";
|
import { store } from "@/utils/store";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
@@ -80,14 +82,9 @@ SplashScreen.setOptions({
|
|||||||
fade: true,
|
fade: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
function redirect(notification: typeof Notifications.Notification) {
|
|
||||||
const url = notification.request.content.data?.url;
|
|
||||||
if (url) {
|
|
||||||
router.push(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useNotificationObserver() {
|
function useNotificationObserver() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.isTV) return;
|
if (Platform.isTV) return;
|
||||||
|
|
||||||
@@ -98,14 +95,17 @@ function useNotificationObserver() {
|
|||||||
if (!isMounted || !response?.notification) {
|
if (!isMounted || !response?.notification) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
redirect(response?.notification);
|
const url = response?.notification.request.content.data?.url;
|
||||||
|
if (url) {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [router]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Platform.isTV) {
|
if (!Platform.isTV) {
|
||||||
@@ -230,6 +230,7 @@ function Layout() {
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const _segments = useSegments();
|
const _segments = useSegments();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
i18n.changeLanguage(
|
i18n.changeLanguage(
|
||||||
@@ -322,9 +323,6 @@ function Layout() {
|
|||||||
responseListener.current =
|
responseListener.current =
|
||||||
Notifications?.addNotificationResponseReceivedListener(
|
Notifications?.addNotificationResponseReceivedListener(
|
||||||
(response: NotificationResponse) => {
|
(response: NotificationResponse) => {
|
||||||
// redirect if internal notification
|
|
||||||
redirect(response?.notification);
|
|
||||||
|
|
||||||
// Currently the notifications supported by the plugin will send data for deep links.
|
// Currently the notifications supported by the plugin will send data for deep links.
|
||||||
const { title, data } = response.notification.request.content;
|
const { title, data } = response.notification.request.content;
|
||||||
writeInfoLog(`Notification ${title} opened`, data);
|
writeInfoLog(`Notification ${title} opened`, data);
|
||||||
@@ -384,77 +382,79 @@ function Layout() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<JellyfinProvider>
|
<JellyfinProvider>
|
||||||
<NetworkStatusProvider>
|
<ServerUrlProvider>
|
||||||
<PlaySettingsProvider>
|
<NetworkStatusProvider>
|
||||||
<LogProvider>
|
<PlaySettingsProvider>
|
||||||
<WebSocketProvider>
|
<LogProvider>
|
||||||
<DownloadProvider>
|
<WebSocketProvider>
|
||||||
<MusicPlayerProvider>
|
<DownloadProvider>
|
||||||
<GlobalModalProvider>
|
<MusicPlayerProvider>
|
||||||
<BottomSheetModalProvider>
|
<GlobalModalProvider>
|
||||||
<IntroSheetProvider>
|
<BottomSheetModalProvider>
|
||||||
<ThemeProvider value={DarkTheme}>
|
<IntroSheetProvider>
|
||||||
<SystemBars style='light' hidden={false} />
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack initialRouteName='(auth)/(tabs)'>
|
<SystemBars style='light' hidden={false} />
|
||||||
<Stack.Screen
|
<Stack initialRouteName='(auth)/(tabs)'>
|
||||||
name='(auth)/(tabs)'
|
<Stack.Screen
|
||||||
options={{
|
name='(auth)/(tabs)'
|
||||||
headerShown: false,
|
options={{
|
||||||
title: "",
|
headerShown: false,
|
||||||
header: () => null,
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/player'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
title: "",
|
||||||
|
header: () => null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='(auth)/now-playing'
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "modal",
|
||||||
|
gestureEnabled: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='login'
|
||||||
|
options={{
|
||||||
|
headerShown: true,
|
||||||
|
title: "",
|
||||||
|
headerTransparent: Platform.OS === "ios",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen name='+not-found' />
|
||||||
|
</Stack>
|
||||||
|
<Toaster
|
||||||
|
duration={4000}
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#262626",
|
||||||
|
borderColor: "#363639",
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
titleStyle: {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
closeButton
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<GlobalModal />
|
||||||
name='(auth)/player'
|
</ThemeProvider>
|
||||||
options={{
|
</IntroSheetProvider>
|
||||||
headerShown: false,
|
</BottomSheetModalProvider>
|
||||||
title: "",
|
</GlobalModalProvider>
|
||||||
header: () => null,
|
</MusicPlayerProvider>
|
||||||
}}
|
</DownloadProvider>
|
||||||
/>
|
</WebSocketProvider>
|
||||||
<Stack.Screen
|
</LogProvider>
|
||||||
name='(auth)/now-playing'
|
</PlaySettingsProvider>
|
||||||
options={{
|
</NetworkStatusProvider>
|
||||||
headerShown: false,
|
</ServerUrlProvider>
|
||||||
presentation: "modal",
|
|
||||||
gestureEnabled: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name='login'
|
|
||||||
options={{
|
|
||||||
headerShown: true,
|
|
||||||
title: "",
|
|
||||||
headerTransparent: Platform.OS === "ios",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen name='+not-found' />
|
|
||||||
</Stack>
|
|
||||||
<Toaster
|
|
||||||
duration={4000}
|
|
||||||
toastOptions={{
|
|
||||||
style: {
|
|
||||||
backgroundColor: "#262626",
|
|
||||||
borderColor: "#363639",
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
titleStyle: {
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
closeButton
|
|
||||||
/>
|
|
||||||
<GlobalModal />
|
|
||||||
</ThemeProvider>
|
|
||||||
</IntroSheetProvider>
|
|
||||||
</BottomSheetModalProvider>
|
|
||||||
</GlobalModalProvider>
|
|
||||||
</MusicPlayerProvider>
|
|
||||||
</DownloadProvider>
|
|
||||||
</WebSocketProvider>
|
|
||||||
</LogProvider>
|
|
||||||
</PlaySettingsProvider>
|
|
||||||
</NetworkStatusProvider>
|
|
||||||
</JellyfinProvider>
|
</JellyfinProvider>
|
||||||
</PersistQueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
640
bun.lock
640
bun.lock
@@ -19,7 +19,7 @@
|
|||||||
"@shopify/flash-list": "2.0.2",
|
"@shopify/flash-list": "2.0.2",
|
||||||
"@tanstack/query-sync-storage-persister": "^5.90.18",
|
"@tanstack/query-sync-storage-persister": "^5.90.18",
|
||||||
"@tanstack/react-pacer": "^0.19.1",
|
"@tanstack/react-pacer": "^0.19.1",
|
||||||
"@tanstack/react-query": "5.90.12",
|
"@tanstack/react-query": "5.90.17",
|
||||||
"@tanstack/react-query-persist-client": "^5.90.18",
|
"@tanstack/react-query-persist-client": "^5.90.18",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"expo": "~54.0.31",
|
"expo": "~54.0.31",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"expo-linear-gradient": "~15.0.8",
|
"expo-linear-gradient": "~15.0.8",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-localization": "~17.0.8",
|
"expo-localization": "~17.0.8",
|
||||||
|
"expo-location": "^19.0.8",
|
||||||
"expo-notifications": "~0.32.16",
|
"expo-notifications": "~0.32.16",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
"expo-screen-orientation": "~9.0.8",
|
"expo-screen-orientation": "~9.0.8",
|
||||||
@@ -47,16 +48,16 @@
|
|||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-system-ui": "~6.0.9",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-task-manager": "~14.0.9",
|
"expo-task-manager": "14.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
"expo-web-browser": "~15.0.10",
|
||||||
"i18next": "^25.0.0",
|
"i18next": "^25.0.0",
|
||||||
"jotai": "2.16.0",
|
"jotai": "2.16.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"nativewind": "^2.0.11",
|
"nativewind": "^2.0.11",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-i18next": "16.5.1",
|
"react-i18next": "16.5.3",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-awesome-slider": "^2.9.0",
|
"react-native-awesome-slider": "^2.9.0",
|
||||||
"react-native-bottom-tabs": "1.1.0",
|
"react-native-bottom-tabs": "1.1.0",
|
||||||
@@ -66,17 +67,17 @@
|
|||||||
"react-native-device-info": "^15.0.0",
|
"react-native-device-info": "^15.0.0",
|
||||||
"react-native-draggable-flatlist": "^4.0.3",
|
"react-native-draggable-flatlist": "^4.0.3",
|
||||||
"react-native-edge-to-edge": "^1.7.0",
|
"react-native-edge-to-edge": "^1.7.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "2.28.0",
|
||||||
"react-native-glass-effect-view": "^1.0.0",
|
"react-native-glass-effect-view": "^1.0.0",
|
||||||
"react-native-google-cast": "^4.9.1",
|
"react-native-google-cast": "^4.9.1",
|
||||||
"react-native-image-colors": "^2.4.0",
|
"react-native-image-colors": "^2.4.0",
|
||||||
"react-native-ios-context-menu": "^3.2.1",
|
"react-native-ios-context-menu": "^3.2.1",
|
||||||
"react-native-ios-utilities": "5.2.0",
|
"react-native-ios-utilities": "5.2.0",
|
||||||
"react-native-mmkv": "4.0.1",
|
"react-native-mmkv": "4.1.1",
|
||||||
"react-native-nitro-modules": "^0.31.5",
|
"react-native-nitro-modules": "0.33.1",
|
||||||
"react-native-pager-view": "^6.9.1",
|
"react-native-pager-view": "^6.9.1",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-reanimated-carousel": "4.0.2",
|
"react-native-reanimated-carousel": "4.0.3",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.18.0",
|
"react-native-screens": "~4.18.0",
|
||||||
"react-native-svg": "15.12.1",
|
"react-native-svg": "15.12.1",
|
||||||
@@ -94,12 +95,12 @@
|
|||||||
"zod": "4.1.13",
|
"zod": "4.1.13",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.5",
|
"@babel/core": "7.28.6",
|
||||||
"@biomejs/biome": "2.3.11",
|
"@biomejs/biome": "2.3.11",
|
||||||
"@react-native-community/cli": "20.1.0",
|
"@react-native-community/cli": "20.1.0",
|
||||||
"@react-native-tvos/config-tv": "0.1.4",
|
"@react-native-tvos/config-tv": "0.1.4",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/lodash": "4.17.21",
|
"@types/lodash": "4.17.23",
|
||||||
"@types/react": "19.1.17",
|
"@types/react": "19.1.17",
|
||||||
"@types/react-test-renderer": "19.1.0",
|
"@types/react-test-renderer": "19.1.0",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
@@ -113,24 +114,23 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"expo-constants": "18.0.13",
|
"expo-constants": "18.0.13",
|
||||||
"expo-task-manager": "~14.0.8",
|
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="],
|
"@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="],
|
||||||
|
|
||||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
"@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="],
|
||||||
|
|
||||||
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
"@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="],
|
||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
"@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="],
|
||||||
|
|
||||||
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
|
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="],
|
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="],
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
|
|
||||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||||
|
|
||||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
|
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
|
||||||
|
|
||||||
@@ -164,11 +164,11 @@
|
|||||||
|
|
||||||
"@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="],
|
"@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="],
|
||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||||
|
|
||||||
"@babel/highlight": ["@babel/highlight@7.25.9", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw=="],
|
"@babel/highlight": ["@babel/highlight@7.25.9", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
||||||
|
|
||||||
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="],
|
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="],
|
||||||
|
|
||||||
@@ -300,13 +300,13 @@
|
|||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
|
||||||
|
|
||||||
"@babel/traverse--for-generate-function-map": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
"@babel/traverse--for-generate-function-map": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
|
"@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
|
||||||
|
|
||||||
@@ -602,7 +602,7 @@
|
|||||||
|
|
||||||
"@tanstack/react-pacer": ["@tanstack/react-pacer@0.19.1", "", { "dependencies": { "@tanstack/pacer": "0.17.1", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-wfGwKLo2gosKr5tsXico+jWJ8LsWsBC8MA1HVtUY/D6dhFduEVizKxRUcvP60I3dRvnoXDbN202g4feJHlivnA=="],
|
"@tanstack/react-pacer": ["@tanstack/react-pacer@0.19.1", "", { "dependencies": { "@tanstack/pacer": "0.17.1", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-wfGwKLo2gosKr5tsXico+jWJ8LsWsBC8MA1HVtUY/D6dhFduEVizKxRUcvP60I3dRvnoXDbN202g4feJHlivnA=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.90.17", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="],
|
||||||
|
|
||||||
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.90.18", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.91.15" }, "peerDependencies": { "@tanstack/react-query": "^5.90.16", "react": "^18 || ^19" } }, "sha512-ToVRTVpjzTrd9S/p7JIvGdLs+Xtz9aDMM/7+TQGSV9notY8Jt64irfAAAkZ05syftLKS+3KPgyKAnHcVeKVbWQ=="],
|
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.90.18", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.91.15" }, "peerDependencies": { "@tanstack/react-query": "^5.90.16", "react": "^18 || ^19" } }, "sha512-ToVRTVpjzTrd9S/p7JIvGdLs+Xtz9aDMM/7+TQGSV9notY8Jt64irfAAAkZ05syftLKS+3KPgyKAnHcVeKVbWQ=="],
|
||||||
|
|
||||||
@@ -634,7 +634,7 @@
|
|||||||
|
|
||||||
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
|
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
|
||||||
|
|
||||||
"@types/lodash": ["@types/lodash@4.17.21", "", {}, "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ=="],
|
"@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||||
|
|
||||||
@@ -1050,6 +1050,8 @@
|
|||||||
|
|
||||||
"expo-localization": ["expo-localization@17.0.8", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g=="],
|
"expo-localization": ["expo-localization@17.0.8", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g=="],
|
||||||
|
|
||||||
|
"expo-location": ["expo-location@19.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA=="],
|
||||||
|
|
||||||
"expo-manifests": ["expo-manifests@1.0.10", "", { "dependencies": { "@expo/config": "~12.0.11", "expo-json-utils": "~0.15.0" }, "peerDependencies": { "expo": "*" } }, "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ=="],
|
"expo-manifests": ["expo-manifests@1.0.10", "", { "dependencies": { "@expo/config": "~12.0.11", "expo-json-utils": "~0.15.0" }, "peerDependencies": { "expo": "*" } }, "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ=="],
|
||||||
|
|
||||||
"expo-modules-autolinking": ["expo-modules-autolinking@3.0.24", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ=="],
|
"expo-modules-autolinking": ["expo-modules-autolinking@3.0.24", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ=="],
|
||||||
@@ -1074,7 +1076,7 @@
|
|||||||
|
|
||||||
"expo-system-ui": ["expo-system-ui@6.0.9", "", { "dependencies": { "@react-native/normalize-colors": "0.81.5", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-eQTYGzw1V4RYiYHL9xDLYID3Wsec2aZS+ypEssmF64D38aDrqbDgz1a2MSlHLQp2jHXSs3FvojhZ9FVela1Zcg=="],
|
"expo-system-ui": ["expo-system-ui@6.0.9", "", { "dependencies": { "@react-native/normalize-colors": "0.81.5", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-eQTYGzw1V4RYiYHL9xDLYID3Wsec2aZS+ypEssmF64D38aDrqbDgz1a2MSlHLQp2jHXSs3FvojhZ9FVela1Zcg=="],
|
||||||
|
|
||||||
"expo-task-manager": ["expo-task-manager@14.0.8", "", { "dependencies": { "unimodules-app-loader": "~6.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HxhyvmulM8px+LQvqIKS85KVx2UodZf5RO+FE2ltpC4mQ5IFkX/ESqiK0grzDa4pVFLyxvs8LjuUKsfB5c39PQ=="],
|
"expo-task-manager": ["expo-task-manager@14.0.9", "", { "dependencies": { "unimodules-app-loader": "~6.0.8" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-GKWtXrkedr4XChHfTm5IyTcSfMtCPxzx89y4CMVqKfyfROATibrE/8UI5j7UC/pUOfFoYlQvulQEvECMreYuUA=="],
|
||||||
|
|
||||||
"expo-updates-interface": ["expo-updates-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg=="],
|
"expo-updates-interface": ["expo-updates-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg=="],
|
||||||
|
|
||||||
@@ -1304,7 +1306,7 @@
|
|||||||
|
|
||||||
"joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
|
"joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
|
||||||
|
|
||||||
"jotai": ["jotai@2.16.0", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-NmkwPBet0SHQ28GBfEb10sqnbVOYyn6DL4iazZgGRDUKxSWL0iqcm+IK4TqTSFC2ixGk+XX2e46Wbv364a3cKg=="],
|
"jotai": ["jotai@2.16.2", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-DH0lBiTXvewsxtqqwjDW6Hg9JPTDnq9LcOsXSFWCAUEt+qj5ohl9iRVX9zQXPPHKLXCdH+5mGvM28fsXMl17/g=="],
|
||||||
|
|
||||||
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||||
|
|
||||||
@@ -1638,7 +1640,7 @@
|
|||||||
|
|
||||||
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
|
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
|
||||||
|
|
||||||
"react-i18next": ["react-i18next@16.5.1", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw=="],
|
"react-i18next": ["react-i18next@16.5.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw=="],
|
||||||
|
|
||||||
"react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="],
|
"react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="],
|
||||||
|
|
||||||
@@ -1674,15 +1676,15 @@
|
|||||||
|
|
||||||
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
|
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
|
||||||
|
|
||||||
"react-native-mmkv": ["react-native-mmkv@4.0.1", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-0JjO0U33b2hngFACsGwxoMCOZlCChP6R42aqvU85kXBaxY/kltSYr0FW9T6lkU3uEkE4IWMV1eLjoJplEY920w=="],
|
"react-native-mmkv": ["react-native-mmkv@4.1.1", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-nYFjM27l7zVhIiyAqWEFRagGASecb13JMIlzAuOeakRRz9GMJ49hCQntUBE2aeuZRE4u4ehSqTOomB0mTF56Ew=="],
|
||||||
|
|
||||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.31.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-h/IbVsK5IH7JkvseihAoz/o5dy6CafvGo7j4jTvAa+gnxZWFtXQZg8EDvu0en88LFAumKd/pcF20dzxMiNOmug=="],
|
"react-native-nitro-modules": ["react-native-nitro-modules@0.33.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Kdo8qiqlkGAEs7fq29i0yiZs0Gf7ucmMiFsH8PH4uzsnSGEt2CQRBJGnQKKMl9vJYL8e7rzA0TZKRwO/L8G/Sg=="],
|
||||||
|
|
||||||
"react-native-pager-view": ["react-native-pager-view@6.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw=="],
|
"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@4.1.3", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*", "react-native-worklets": ">=0.5.0" } }, "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg=="],
|
"react-native-reanimated": ["react-native-reanimated@4.1.3", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*", "react-native-worklets": ">=0.5.0" } }, "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg=="],
|
||||||
|
|
||||||
"react-native-reanimated-carousel": ["react-native-reanimated-carousel@4.0.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.70.3", "react-native-gesture-handler": ">=2.9.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q=="],
|
"react-native-reanimated-carousel": ["react-native-reanimated-carousel@4.0.3", "", { "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.70.3", "react-native-gesture-handler": ">=2.9.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-YZXlvZNghR5shFcI9hTA7h7bEhh97pfUSLZvLBAshpbkuYwJDKmQXejO/199T6hqGq0wCRwR0CWf2P4Vs6A4Fw=="],
|
||||||
|
|
||||||
"react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="],
|
"react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="],
|
||||||
|
|
||||||
@@ -1948,7 +1950,7 @@
|
|||||||
|
|
||||||
"unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="],
|
"unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="],
|
||||||
|
|
||||||
"unimodules-app-loader": ["unimodules-app-loader@6.0.7", "", {}, "sha512-23iwxmh6/y54PRGJt/xjsOpPK8vlfusBisi3yaVSK22pxg5DmiL/+IHCtbb/crHC+gqdItcy1OoRsZQHfNSBaw=="],
|
"unimodules-app-loader": ["unimodules-app-loader@6.0.8", "", {}, "sha512-fqS8QwT/MC/HAmw1NKCHdzsPA6WaLm0dNmoC5Pz6lL+cDGYeYCNdHMO9fy08aL2ZD7cVkNM0pSR/AoNRe+rslA=="],
|
||||||
|
|
||||||
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
|
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
|
||||||
|
|
||||||
@@ -2040,16 +2042,76 @@
|
|||||||
|
|
||||||
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
|
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
|
||||||
|
|
||||||
"@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
"@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
|
"@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-remap-async-to-generator/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
"@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-generator-functions/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-computed-properties/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-destructuring/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-runtime/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@babel/traverse--for-generate-function-map/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/traverse--for-generate-function-map/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/traverse--for-generate-function-map/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/traverse--for-generate-function-map/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/traverse--for-generate-function-map/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@expo/cli/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
|
"@expo/cli/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
|
||||||
|
|
||||||
"@expo/cli/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
"@expo/cli/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||||
@@ -2098,6 +2160,12 @@
|
|||||||
|
|
||||||
"@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
|
"@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
"@expo/metro-config/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
|
"@expo/metro-config/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
|
||||||
|
|
||||||
"@expo/metro-config/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
"@expo/metro-config/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||||
@@ -2122,6 +2190,8 @@
|
|||||||
|
|
||||||
"@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
"@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
"@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
"@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
|
|
||||||
"@jimp/png/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
"@jimp/png/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||||
@@ -2140,6 +2210,16 @@
|
|||||||
|
|
||||||
"@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
|
"@react-native/babel-plugin-codegen/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/metro": ["metro@0.83.2", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.2", "metro-cache": "0.83.2", "metro-cache-key": "0.83.2", "metro-config": "0.83.2", "metro-core": "0.83.2", "metro-file-map": "0.83.2", "metro-resolver": "0.83.2", "metro-runtime": "0.83.2", "metro-source-map": "0.83.2", "metro-symbolicate": "0.83.2", "metro-transform-plugins": "0.83.2", "metro-transform-worker": "0.83.2", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-HQgs9H1FyVbRptNSMy/ImchTTE5vS2MSqLoOo7hbDoBq6hPPZokwJvBMwrYSxdjQZmLXz2JFZtdvS+ZfgTc9yw=="],
|
"@react-native/community-cli-plugin/metro": ["metro@0.83.2", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.2", "metro-cache": "0.83.2", "metro-cache-key": "0.83.2", "metro-config": "0.83.2", "metro-core": "0.83.2", "metro-file-map": "0.83.2", "metro-resolver": "0.83.2", "metro-runtime": "0.83.2", "metro-source-map": "0.83.2", "metro-symbolicate": "0.83.2", "metro-transform-plugins": "0.83.2", "metro-transform-worker": "0.83.2", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-HQgs9H1FyVbRptNSMy/ImchTTE5vS2MSqLoOo7hbDoBq6hPPZokwJvBMwrYSxdjQZmLXz2JFZtdvS+ZfgTc9yw=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.2", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.2", "metro-cache": "0.83.2", "metro-core": "0.83.2", "metro-runtime": "0.83.2", "yaml": "^2.6.1" } }, "sha512-1FjCcdBe3e3D08gSSiU9u3Vtxd7alGH3x/DNFqWDFf5NouX4kLgbVloDDClr1UrLz62c0fHh2Vfr9ecmrOZp+g=="],
|
"@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.2", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.2", "metro-cache": "0.83.2", "metro-core": "0.83.2", "metro-runtime": "0.83.2", "yaml": "^2.6.1" } }, "sha512-1FjCcdBe3e3D08gSSiU9u3Vtxd7alGH3x/DNFqWDFf5NouX4kLgbVloDDClr1UrLz62c0fHh2Vfr9ecmrOZp+g=="],
|
||||||
@@ -2162,7 +2242,19 @@
|
|||||||
|
|
||||||
"@react-navigation/native-stack/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
"@react-navigation/native-stack/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||||
|
|
||||||
"@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
|
"@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.17", "", {}, "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ=="],
|
||||||
|
|
||||||
|
"@types/babel__core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@types/babel__core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@types/babel__generator/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@types/babel__template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@types/babel__template/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@types/babel__traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
@@ -2174,6 +2266,14 @@
|
|||||||
|
|
||||||
"babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
"babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
|
|
||||||
|
"babel-plugin-jest-hoist/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"babel-plugin-jest-hoist/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"babel-plugin-polyfill-corejs2/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"babel-plugin-react-compiler/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"babel-preset-expo/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
"babel-preset-expo/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||||
@@ -2230,6 +2330,12 @@
|
|||||||
|
|
||||||
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"jest-message-util/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
"jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
|
|
||||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||||
@@ -2244,14 +2350,52 @@
|
|||||||
|
|
||||||
"logkitty/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
"logkitty/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
||||||
|
|
||||||
|
"metro/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"metro/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"metro/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"metro/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"metro/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"metro/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
|
"metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
|
||||||
|
|
||||||
"metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
"metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
||||||
|
|
||||||
"metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
"metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
"metro-babel-transformer/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
"metro-babel-transformer/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
||||||
|
|
||||||
|
"metro-source-map/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"metro-source-map/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"nativewind/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
"nativewind/@babel/types": ["@babel/types@7.19.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } }, "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA=="],
|
"nativewind/@babel/types": ["@babel/types@7.19.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } }, "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA=="],
|
||||||
|
|
||||||
"nativewind/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"nativewind/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
@@ -2260,6 +2404,8 @@
|
|||||||
|
|
||||||
"npm-package-arg/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
"npm-package-arg/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||||
|
|
||||||
|
"parse-json/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"patch-package/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
"patch-package/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||||
|
|
||||||
"patch-package/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
"patch-package/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||||
@@ -2340,12 +2486,146 @@
|
|||||||
|
|
||||||
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-define-polyfill-provider/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/helper-remap-async-to-generator/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-remap-async-to-generator/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-remap-async-to-generator/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-remap-async-to-generator/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-wrap-function/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
"@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
"@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||||
|
|
||||||
"@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
"@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||||
|
|
||||||
"@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
"@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-generator-functions/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-classes/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-computed-properties/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-computed-properties/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-computed-properties/@babel/template/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-destructuring/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-destructuring/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-destructuring/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-destructuring/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-function-name/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@expo/cli/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
"@expo/cli/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||||
|
|
||||||
"@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
"@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||||
@@ -2366,6 +2646,24 @@
|
|||||||
|
|
||||||
"@expo/fingerprint/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
"@expo/fingerprint/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/generator/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@expo/metro-config/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
"@expo/metro-config/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||||
|
|
||||||
"@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
"@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||||
@@ -2386,8 +2684,90 @@
|
|||||||
|
|
||||||
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@react-native-community/cli-server-api/open/is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="],
|
"@react-native-community/cli-server-api/open/is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="],
|
||||||
|
|
||||||
|
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/template/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/parser/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
|
"@react-native/community-cli-plugin/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
"@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
|
||||||
@@ -2442,6 +2822,14 @@
|
|||||||
|
|
||||||
"ansi-fragments/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
"ansi-fragments/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
||||||
|
|
||||||
|
"babel-plugin-jest-hoist/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"babel-plugin-jest-hoist/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"babel-preset-expo/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"babel-preset-expo/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
@@ -2472,6 +2860,24 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/parser/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -2486,10 +2892,86 @@
|
|||||||
|
|
||||||
"logkitty/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
"logkitty/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
"metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
||||||
|
|
||||||
|
"metro-source-map/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"metro-source-map/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"metro-source-map/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro-source-map/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/generator/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/template/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/template/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/template/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
|
"metro/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"metro/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"metro/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
"metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
"metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
||||||
|
|
||||||
|
"nativewind/@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"nativewind/@babel/generator/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
"patch-package/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
"patch-package/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
||||||
@@ -2522,6 +3004,42 @@
|
|||||||
|
|
||||||
"@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
"@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-async-to-generator/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs/@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
"@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||||
|
|
||||||
"@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
"@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||||
@@ -2534,6 +3052,10 @@
|
|||||||
|
|
||||||
"@expo/cli/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"@expo/cli/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@expo/metro-config/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
"@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
"@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||||
|
|
||||||
"@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
"@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||||
@@ -2546,6 +3068,26 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@jest/transform/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@react-native/babel-preset/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@react-native/codegen/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/core/@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
"@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
|
||||||
|
|
||||||
"@react-native/community-cli-plugin/metro/metro-source-map/ob1": ["ob1@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-XlK3w4M+dwd1g1gvHzVbxiXEbUllRONEgcF2uEO0zm4nxa0eKlh41c6N65q1xbiDOeKKda1tvNOAD33fNjyvCg=="],
|
"@react-native/community-cli-plugin/metro/metro-source-map/ob1": ["ob1@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-XlK3w4M+dwd1g1gvHzVbxiXEbUllRONEgcF2uEO0zm4nxa0eKlh41c6N65q1xbiDOeKKda1tvNOAD33fNjyvCg=="],
|
||||||
@@ -2570,6 +3112,14 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"babel-preset-expo/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"babel-preset-expo/@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
|
"babel-preset-expo/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
|
"babel-preset-expo/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
@@ -2582,6 +3132,10 @@
|
|||||||
|
|
||||||
"expo-manifests/@expo/config/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
"expo-manifests/@expo/config/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"istanbul-lib-instrument/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
"log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
"log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
"log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
@@ -2592,6 +3146,22 @@
|
|||||||
|
|
||||||
"logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
"logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"metro-babel-transformer/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"metro-transform-plugins/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"metro-transform-worker/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"metro/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"metro/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
"serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
@@ -2616,6 +3186,10 @@
|
|||||||
|
|
||||||
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
|
"@react-native/community-cli-plugin/metro/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
"ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
"ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { View, type ViewProps } from "react-native";
|
import { Platform, StyleSheet, View, type ViewProps } from "react-native";
|
||||||
|
import { GlassEffectView } from "react-native-glass-effect-view";
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
@@ -13,6 +14,30 @@ export const Badge: React.FC<Props> = ({
|
|||||||
variant = "purple",
|
variant = "purple",
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
const content = (
|
||||||
|
<View style={styles.content}>
|
||||||
|
{iconLeft && <View style={styles.iconLeft}>{iconLeft}</View>}
|
||||||
|
<Text
|
||||||
|
className={`
|
||||||
|
text-xs
|
||||||
|
${variant === "purple" && "text-white"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Platform.OS === "ios") {
|
||||||
|
return (
|
||||||
|
<View {...props} style={[styles.container, props.style]}>
|
||||||
|
<GlassEffectView style={{ borderRadius: 100 }}>
|
||||||
|
{content}
|
||||||
|
</GlassEffectView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
{...props}
|
{...props}
|
||||||
@@ -34,3 +59,23 @@ export const Badge: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
overflow: "hidden",
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
flexShrink: 1,
|
||||||
|
flexGrow: 0,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 50,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
iconLeft: {
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import type {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { type Href, router } from "expo-router";
|
import { type Href } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Alert, Platform, Switch, View, type ViewProps } from "react-native";
|
import { Alert, Platform, Switch, View, type ViewProps } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
@@ -62,6 +63,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [queue, _setQueue] = useAtom(queueAtom);
|
const [queue, _setQueue] = useAtom(queueAtom);
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
const router = useRouter();
|
||||||
const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
|
const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
|
||||||
|
|
||||||
const { processes, startBackgroundDownload, downloadedItems } = useDownload();
|
const { processes, startBackgroundDownload, downloadedItems } = useDownload();
|
||||||
@@ -170,9 +172,11 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
firstItem.Type !== "Episode"
|
firstItem.Type !== "Episode"
|
||||||
? "/downloads"
|
? "/downloads"
|
||||||
: ({
|
: ({
|
||||||
pathname: `/downloads/${firstItem.SeriesId}`,
|
pathname: "/series/[id]",
|
||||||
params: {
|
params: {
|
||||||
episodeSeasonIndex: firstItem.ParentIndexNumber,
|
id: firstItem.SeriesId!,
|
||||||
|
seasonIndex: firstItem.ParentIndexNumber?.toString(),
|
||||||
|
offline: "true",
|
||||||
},
|
},
|
||||||
} as Href),
|
} as Href),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// GenreTags.tsx
|
// GenreTags.tsx
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
|
Platform,
|
||||||
type StyleProp,
|
type StyleProp,
|
||||||
|
StyleSheet,
|
||||||
type TextStyle,
|
type TextStyle,
|
||||||
View,
|
View,
|
||||||
type ViewProps,
|
type ViewProps,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { GlassEffectView } from "react-native-glass-effect-view";
|
||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
|
|
||||||
interface TagProps {
|
interface TagProps {
|
||||||
@@ -20,6 +23,23 @@ export const Tag: React.FC<
|
|||||||
textStyle?: StyleProp<TextStyle>;
|
textStyle?: StyleProp<TextStyle>;
|
||||||
} & ViewProps
|
} & ViewProps
|
||||||
> = ({ text, textClass, textStyle, ...props }) => {
|
> = ({ text, textClass, textStyle, ...props }) => {
|
||||||
|
if (Platform.OS === "ios") {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<GlassEffectView style={styles.glass}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{text}</Text>
|
||||||
|
</View>
|
||||||
|
</GlassEffectView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='bg-neutral-800 rounded-full px-2 py-1' {...props}>
|
<View className='bg-neutral-800 rounded-full px-2 py-1' {...props}>
|
||||||
<Text className={textClass} style={textStyle}>
|
<Text className={textClass} style={textStyle}>
|
||||||
@@ -29,6 +49,16 @@ export const Tag: React.FC<
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: 50,
|
||||||
|
},
|
||||||
|
glass: {
|
||||||
|
borderRadius: 50,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const Tags: React.FC<
|
export const Tags: React.FC<
|
||||||
TagProps & { tagProps?: ViewProps } & ViewProps
|
TagProps & { tagProps?: ViewProps } & ViewProps
|
||||||
> = ({ tags, textClass = "text-xs", tagProps, ...props }) => {
|
> = ({ tags, textClass = "text-xs", tagProps, ...props }) => {
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {
|
|||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { router } from "expo-router";
|
|
||||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Linking, Platform, TouchableOpacity, View } from "react-native";
|
import { Linking, Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
|
|
||||||
export interface IntroSheetRef {
|
export interface IntroSheetRef {
|
||||||
@@ -24,6 +24,7 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
|
|||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
present: () => {
|
present: () => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
|
|||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||||
import { AddToFavorites } from "./AddToFavorites";
|
import { AddToFavorites } from "./AddToFavorites";
|
||||||
@@ -45,13 +46,13 @@ export type SelectedOptions = {
|
|||||||
|
|
||||||
interface ItemContentProps {
|
interface ItemContentProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
isOffline: boolean;
|
|
||||||
itemWithSources?: BaseItemDto | null;
|
itemWithSources?: BaseItemDto | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||||
({ item, isOffline, itemWithSources }) => {
|
({ item, itemWithSources }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { orientation } = useOrientation();
|
const { orientation } = useOrientation();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
@@ -228,7 +229,6 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
|||||||
<PlayButton
|
<PlayButton
|
||||||
selectedOptions={selectedOptions}
|
selectedOptions={selectedOptions}
|
||||||
item={item}
|
item={item}
|
||||||
isOffline={isOffline}
|
|
||||||
colors={itemColors}
|
colors={itemColors}
|
||||||
/>
|
/>
|
||||||
<View className='w-1' />
|
<View className='w-1' />
|
||||||
@@ -243,11 +243,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{item.Type === "Episode" && (
|
{item.Type === "Episode" && (
|
||||||
<SeasonEpisodesCarousel
|
<SeasonEpisodesCarousel item={item} loading={loading} />
|
||||||
item={item}
|
|
||||||
loading={loading}
|
|
||||||
isOffline={isOffline}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isOffline &&
|
{!isOffline &&
|
||||||
@@ -264,7 +260,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
|||||||
<CurrentSeries item={item} className='mb-2' />
|
<CurrentSeries item={item} className='mb-2' />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ItemPeopleSections item={item} isOffline={isOffline} />
|
<ItemPeopleSections item={item} />
|
||||||
|
|
||||||
{!isOffline && <SimilarItems itemId={item.Id} />}
|
{!isOffline && <SimilarItems itemId={item.Id} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ export type ToggleOption = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Option = RadioOption | ToggleOption;
|
export type ActionOption = {
|
||||||
|
type: "action";
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Option = RadioOption | ToggleOption | ActionOption;
|
||||||
|
|
||||||
// Option group structure
|
// Option group structure
|
||||||
export type OptionGroup = {
|
export type OptionGroup = {
|
||||||
@@ -64,7 +71,10 @@ const OptionItem: React.FC<{ option: Option; isLast?: boolean }> = ({
|
|||||||
isLast,
|
isLast,
|
||||||
}) => {
|
}) => {
|
||||||
const isToggle = option.type === "toggle";
|
const isToggle = option.type === "toggle";
|
||||||
const handlePress = isToggle ? option.onToggle : option.onPress;
|
const isAction = option.type === "action";
|
||||||
|
const handlePress = isToggle
|
||||||
|
? option.onToggle
|
||||||
|
: (option as RadioOption | ActionOption).onPress;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -76,7 +86,7 @@ const OptionItem: React.FC<{ option: Option; isLast?: boolean }> = ({
|
|||||||
<Text className='flex-1 text-white'>{option.label}</Text>
|
<Text className='flex-1 text-white'>{option.label}</Text>
|
||||||
{isToggle ? (
|
{isToggle ? (
|
||||||
<ToggleSwitch value={option.value} />
|
<ToggleSwitch value={option.value} />
|
||||||
) : option.selected ? (
|
) : isAction ? null : (option as RadioOption).selected ? (
|
||||||
<Ionicons name='checkmark-circle' size={24} color='#9333ea' />
|
<Ionicons name='checkmark-circle' size={24} color='#9333ea' />
|
||||||
) : (
|
) : (
|
||||||
<Ionicons name='ellipse-outline' size={24} color='#6b7280' />
|
<Ionicons name='ellipse-outline' size={24} color='#6b7280' />
|
||||||
@@ -150,6 +160,15 @@ const BottomSheetContent: React.FC<{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (option.type === "action") {
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
onPress: () => {
|
||||||
|
option.onPress();
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return option;
|
return option;
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
@@ -225,6 +244,9 @@ const PlatformDropdownComponent = ({
|
|||||||
const toggleOptions = group.options.filter(
|
const toggleOptions = group.options.filter(
|
||||||
(opt) => opt.type === "toggle",
|
(opt) => opt.type === "toggle",
|
||||||
) as ToggleOption[];
|
) as ToggleOption[];
|
||||||
|
const actionOptions = group.options.filter(
|
||||||
|
(opt) => opt.type === "action",
|
||||||
|
) as ActionOption[];
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
@@ -291,6 +313,21 @@ const PlatformDropdownComponent = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add Buttons for action options (no icon)
|
||||||
|
actionOptions.forEach((option, optionIndex) => {
|
||||||
|
items.push(
|
||||||
|
<Button
|
||||||
|
key={`action-${groupIndex}-${optionIndex}`}
|
||||||
|
onPress={() => {
|
||||||
|
option.onPress();
|
||||||
|
}}
|
||||||
|
disabled={option.disabled}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
})}
|
})}
|
||||||
</ContextMenu.Items>
|
</ContextMenu.Items>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useActionSheet } from "@expo/react-native-action-sheet";
|
|||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
import { BottomSheetView } from "@gorhom/bottom-sheet";
|
import { BottomSheetView } from "@gorhom/bottom-sheet";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -24,11 +23,13 @@ import Animated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
||||||
import { getDownloadedItemById } from "@/providers/Downloads/database";
|
import { getDownloadedItemById } from "@/providers/Downloads/database";
|
||||||
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
import { useGlobalModal } from "@/providers/GlobalModalProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
||||||
@@ -44,7 +45,6 @@ import type { SelectedOptions } from "./ItemContent";
|
|||||||
interface Props extends React.ComponentProps<typeof TouchableOpacity> {
|
interface Props extends React.ComponentProps<typeof TouchableOpacity> {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
selectedOptions: SelectedOptions;
|
selectedOptions: SelectedOptions;
|
||||||
isOffline?: boolean;
|
|
||||||
colors?: ThemeColors;
|
colors?: ThemeColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +54,9 @@ const MIN_PLAYBACK_WIDTH = 15;
|
|||||||
export const PlayButton: React.FC<Props> = ({
|
export const PlayButton: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
selectedOptions,
|
selectedOptions,
|
||||||
isOffline,
|
|
||||||
colors,
|
colors,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const client = useRemoteMediaClient();
|
const client = useRemoteMediaClient();
|
||||||
const mediaStatus = useMediaStatus();
|
const mediaStatus = useMediaStatus();
|
||||||
@@ -300,6 +300,19 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
// Check if item is downloaded
|
// Check if item is downloaded
|
||||||
const downloadedItem = item.Id ? getDownloadedItemById(item.Id) : undefined;
|
const downloadedItem = item.Id ? getDownloadedItemById(item.Id) : undefined;
|
||||||
|
|
||||||
|
// If already in offline mode, play downloaded file directly
|
||||||
|
if (isOffline && downloadedItem) {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
itemId: item.Id!,
|
||||||
|
offline: "true",
|
||||||
|
playbackPosition:
|
||||||
|
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||||
|
});
|
||||||
|
goToPlayer(queryParams.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If online but file is downloaded, ask user which version to play
|
||||||
if (downloadedItem) {
|
if (downloadedItem) {
|
||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
// Show bottom sheet for Android
|
// Show bottom sheet for Android
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
@@ -14,6 +13,7 @@ import Animated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { RoundButton } from "./RoundButton";
|
|||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[];
|
||||||
isOffline?: boolean;
|
|
||||||
size?: "default" | "large";
|
size?: "default" | "large";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +25,7 @@ import Animated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
||||||
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
|
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
|
||||||
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
|
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { BlurView, type BlurViewProps } from "expo-blur";
|
import { BlurView, type BlurViewProps } from "expo-blur";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { Pressable, type PressableProps } from "react-native-gesture-handler";
|
import { Pressable, type PressableProps } from "react-native-gesture-handler";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
|
||||||
interface Props extends BlurViewProps {
|
interface Props extends BlurViewProps {
|
||||||
background?: "blur" | "transparent";
|
background?: "blur" | "transparent";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useRouter, useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { type PropsWithChildren } from "react";
|
import { type PropsWithChildren } from "react";
|
||||||
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||||
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
|
||||||
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useRouter, useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import { type PropsWithChildren, useCallback } from "react";
|
import { type PropsWithChildren, useCallback } from "react";
|
||||||
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useFavorite } from "@/hooks/useFavorite";
|
import { useFavorite } from "@/hooks/useFavorite";
|
||||||
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
|
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
|
||||||
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
isOffline?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const itemRouter = (item: BaseItemDto, from: string) => {
|
export const itemRouter = (item: BaseItemDto, from: string) => {
|
||||||
@@ -134,26 +136,20 @@ export const getItemNavigation = (item: BaseItemDto, _from: string) => {
|
|||||||
|
|
||||||
export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||||
item,
|
item,
|
||||||
isOffline = false,
|
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const markAsPlayedStatus = useMarkAsPlayed([item]);
|
const markAsPlayedStatus = useMarkAsPlayed([item]);
|
||||||
const { isFavorite, toggleFavorite } = useFavorite(item);
|
const { isFavorite, toggleFavorite } = useFavorite(item);
|
||||||
|
const router = useRouter();
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
|
const { deleteFile } = useDownload();
|
||||||
|
|
||||||
const from = (segments as string[])[2] || "(home)";
|
const from = (segments as string[])[2] || "(home)";
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
// For offline mode, we still need to use query params
|
|
||||||
if (isOffline) {
|
|
||||||
const url = `${itemRouter(item, from)}&offline=true`;
|
|
||||||
router.push(url as any);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force music libraries to navigate via the explicit string route.
|
// Force music libraries to navigate via the explicit string route.
|
||||||
// This avoids losing the dynamic [libraryId] param when going through a nested navigator.
|
// This avoids losing the dynamic [libraryId] param when going through a nested navigator.
|
||||||
if ("CollectionType" in item && item.CollectionType === "music") {
|
if ("CollectionType" in item && item.CollectionType === "music") {
|
||||||
@@ -163,7 +159,7 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
|
|
||||||
const navigation = getItemNavigation(item, from);
|
const navigation = getItemNavigation(item, from);
|
||||||
router.push(navigation as any);
|
router.push(navigation as any);
|
||||||
}, [from, isOffline, item, router]);
|
}, [from, item, router]);
|
||||||
|
|
||||||
const showActionSheet = useCallback(() => {
|
const showActionSheet = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
@@ -179,14 +175,19 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
"Mark as Played",
|
"Mark as Played",
|
||||||
"Mark as Not Played",
|
"Mark as Not Played",
|
||||||
isFavorite ? "Unmark as Favorite" : "Mark as Favorite",
|
isFavorite ? "Unmark as Favorite" : "Mark as Favorite",
|
||||||
|
...(isOffline ? ["Delete Download"] : []),
|
||||||
"Cancel",
|
"Cancel",
|
||||||
];
|
];
|
||||||
const cancelButtonIndex = options.length - 1;
|
const cancelButtonIndex = options.length - 1;
|
||||||
|
const destructiveButtonIndex = isOffline
|
||||||
|
? cancelButtonIndex - 1
|
||||||
|
: undefined;
|
||||||
|
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options,
|
options,
|
||||||
cancelButtonIndex,
|
cancelButtonIndex,
|
||||||
|
destructiveButtonIndex,
|
||||||
},
|
},
|
||||||
async (selectedIndex) => {
|
async (selectedIndex) => {
|
||||||
if (selectedIndex === 0) {
|
if (selectedIndex === 0) {
|
||||||
@@ -195,6 +196,8 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
await markAsPlayedStatus(false);
|
await markAsPlayedStatus(false);
|
||||||
} else if (selectedIndex === 2) {
|
} else if (selectedIndex === 2) {
|
||||||
toggleFavorite();
|
toggleFavorite();
|
||||||
|
} else if (isOffline && selectedIndex === 3 && item.Id) {
|
||||||
|
deleteFile(item.Id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -203,6 +206,9 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
isFavorite,
|
isFavorite,
|
||||||
markAsPlayedStatus,
|
markAsPlayedStatus,
|
||||||
toggleFavorite,
|
toggleFavorite,
|
||||||
|
isOffline,
|
||||||
|
deleteFile,
|
||||||
|
item.Id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { calculateSmoothedETA } from "@/providers/Downloads/hooks/useDownloadSpeedCalculator";
|
import { calculateSmoothedETA } from "@/providers/Downloads/hooks/useDownloadSpeedCalculator";
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item }) => {
|
|||||||
return (
|
return (
|
||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
item={item}
|
item={item}
|
||||||
isOffline={true}
|
|
||||||
onLongPress={showActionSheet}
|
onLongPress={showActionSheet}
|
||||||
className='flex flex-col mb-4'
|
className='flex flex-col mb-4'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
|||||||
}, [showActionSheetWithOptions, handleDeleteFile]);
|
}, [showActionSheetWithOptions, handleDeleteFile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableItemRouter onLongPress={showActionSheet} item={item} isOffline>
|
<TouchableItemRouter onLongPress={showActionSheet} item={item}>
|
||||||
{base64Image ? (
|
{base64Image ? (
|
||||||
<View className='relative w-28 aspect-[10/15] rounded-lg overflow-hidden mr-2 border border-neutral-900'>
|
<View className='relative w-28 aspect-[10/15] rounded-lg overflow-hidden mr-2 border border-neutral-900'>
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { useActionSheet } from "@expo/react-native-action-sheet";
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { router } from "expo-router";
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { DownloadSize } from "@/components/downloads/DownloadSize";
|
import { DownloadSize } from "@/components/downloads/DownloadSize";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
@@ -14,6 +14,7 @@ import { Text } from "../common/Text";
|
|||||||
export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
|
export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
|
||||||
const { deleteItems } = useDownload();
|
const { deleteItems } = useDownload();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const base64Image = useMemo(() => {
|
const base64Image = useMemo(() => {
|
||||||
return storage.getString(items[0].SeriesId!);
|
return storage.getString(items[0].SeriesId!);
|
||||||
@@ -46,7 +47,12 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push(`/downloads/${items[0].SeriesId}`)}
|
onPress={() =>
|
||||||
|
router.push({
|
||||||
|
pathname: "/series/[id]",
|
||||||
|
params: { id: items[0].SeriesId!, offline: "true" },
|
||||||
|
})
|
||||||
|
}
|
||||||
onLongPress={showActionSheet}
|
onLongPress={showActionSheet}
|
||||||
>
|
>
|
||||||
{base64Image ? (
|
{base64Image ? (
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { Api } from "@jellyfin/sdk";
|
|||||||
import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
|
import type { BaseItemKind } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
@@ -10,6 +9,7 @@ import { Text, View } from "react-native";
|
|||||||
// PNG ASSET
|
// PNG ASSET
|
||||||
import heart from "@/assets/icons/heart.fill.png";
|
import heart from "@/assets/icons/heart.fill.png";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { InfiniteScrollingCollectionList } from "./InfiniteScrollingCollectionList";
|
import { InfiniteScrollingCollectionList } from "./InfiniteScrollingCollectionList";
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
getUserViewsApi,
|
getUserViewsApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { type QueryFunction, useQuery } from "@tanstack/react-query";
|
import { type QueryFunction, useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigation, useRouter, useSegments } from "expo-router";
|
import { useNavigation, useSegments } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -33,6 +33,7 @@ import { StreamystatsRecommendations } from "@/components/home/StreamystatsRecom
|
|||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
getUserViewsApi,
|
getUserViewsApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { type QueryFunction, useQuery } from "@tanstack/react-query";
|
import { type QueryFunction, useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigation, useRouter, useSegments } from "expo-router";
|
import { useNavigation, useSegments } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -35,6 +35,7 @@ import { StreamystatsRecommendations } from "@/components/home/StreamystatsRecom
|
|||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter, useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { Dimensions, View, type ViewProps } from "react-native";
|
import { Dimensions, View, type ViewProps } from "react-native";
|
||||||
@@ -16,6 +16,7 @@ import Carousel, {
|
|||||||
type ICarouselInstance,
|
type ICarouselInstance,
|
||||||
Pagination,
|
Pagination,
|
||||||
} from "react-native-reanimated-carousel";
|
} from "react-native-reanimated-carousel";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ interface Props extends ViewProps {
|
|||||||
queryKey: QueryKey;
|
queryKey: QueryKey;
|
||||||
queryFn: QueryFunction<BaseItemDto[]>;
|
queryFn: QueryFunction<BaseItemDto[]>;
|
||||||
hideIfEmpty?: boolean;
|
hideIfEmpty?: boolean;
|
||||||
isOffline?: boolean;
|
|
||||||
scrollY?: number; // For lazy loading
|
scrollY?: number; // For lazy loading
|
||||||
enableLazyLoading?: boolean; // Enable/disable lazy loading
|
enableLazyLoading?: boolean; // Enable/disable lazy loading
|
||||||
}
|
}
|
||||||
@@ -33,7 +32,6 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
queryFn,
|
queryFn,
|
||||||
queryKey,
|
queryKey,
|
||||||
hideIfEmpty = false,
|
hideIfEmpty = false,
|
||||||
isOffline = false,
|
|
||||||
scrollY = 0,
|
scrollY = 0,
|
||||||
enableLazyLoading = false,
|
enableLazyLoading = false,
|
||||||
...props
|
...props
|
||||||
@@ -106,7 +104,6 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
<TouchableItemRouter
|
<TouchableItemRouter
|
||||||
item={item}
|
item={item}
|
||||||
key={item.Id}
|
key={item.Id}
|
||||||
isOffline={isOffline}
|
|
||||||
className={`mr-2
|
className={`mr-2
|
||||||
${orientation === "horizontal" ? "w-44" : "w-28"}
|
${orientation === "horizontal" ? "w-44" : "w-28"}
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -8,17 +8,14 @@ import { InteractionManager, View, type ViewProps } from "react-native";
|
|||||||
import { MoreMoviesWithActor } from "@/components/MoreMoviesWithActor";
|
import { MoreMoviesWithActor } from "@/components/MoreMoviesWithActor";
|
||||||
import { CastAndCrew } from "@/components/series/CastAndCrew";
|
import { CastAndCrew } from "@/components/series/CastAndCrew";
|
||||||
import { useItemPeopleQuery } from "@/hooks/useItemPeopleQuery";
|
import { useItemPeopleQuery } from "@/hooks/useItemPeopleQuery";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
isOffline: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ItemPeopleSections: React.FC<Props> = ({
|
export const ItemPeopleSections: React.FC<Props> = ({ item, ...props }) => {
|
||||||
item,
|
const isOffline = useOfflineMode();
|
||||||
isOffline,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useRouter, useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import Poster from "@/components/posters/Poster";
|
import Poster from "@/components/posters/Poster";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { router, useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { TouchableOpacity, type ViewProps } from "react-native";
|
import { TouchableOpacity, type ViewProps } from "react-native";
|
||||||
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
||||||
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
|
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import {
|
import {
|
||||||
COMPANY_LOGO_IMAGE_FILTER,
|
COMPANY_LOGO_IMAGE_FILTER,
|
||||||
@@ -16,6 +17,7 @@ const CompanySlide: React.FC<
|
|||||||
> = ({ slide, data, ...props }) => {
|
> = ({ slide, data, ...props }) => {
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi } = useJellyseerr();
|
||||||
|
const router = useRouter();
|
||||||
const from = (segments as string[])[2] || "(home)";
|
const from = (segments as string[])[2] || "(home)";
|
||||||
|
|
||||||
const navigate = useCallback(
|
const navigate = useCallback(
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { router, useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { TouchableOpacity, type ViewProps } from "react-native";
|
import { TouchableOpacity, type ViewProps } from "react-native";
|
||||||
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
|
||||||
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
|
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
|
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
|
||||||
import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
|
import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
|
||||||
@@ -13,6 +14,7 @@ import { genreColorMap } from "@/utils/jellyseerr/src/components/Discover/consta
|
|||||||
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const { jellyseerrApi } = useJellyseerr();
|
const { jellyseerrApi } = useJellyseerr();
|
||||||
|
const router = useRouter();
|
||||||
const from = (segments as string[])[2] || "(home)";
|
const from = (segments as string[])[2] || "(home)";
|
||||||
|
|
||||||
const navigate = useCallback(
|
const navigate = useCallback(
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
|||||||
_itemType = "Series";
|
_itemType = "Series";
|
||||||
} else if (library.CollectionType === "boxsets") {
|
} else if (library.CollectionType === "boxsets") {
|
||||||
_itemType = "BoxSet";
|
_itemType = "BoxSet";
|
||||||
|
} else if (library.CollectionType === "homevideos") {
|
||||||
|
_itemType = "Video";
|
||||||
|
} else if (library.CollectionType === "musicvideos") {
|
||||||
|
_itemType = "MusicVideo";
|
||||||
}
|
}
|
||||||
|
|
||||||
return _itemType;
|
return _itemType;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +22,7 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useMusicPlayer } from "@/providers/MusicPlayerProvider";
|
import { useMusicPlayer } from "@/providers/MusicPlayerProvider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { getLocalPath } from "@/providers/AudioStorage";
|
import { getLocalPath } from "@/providers/AudioStorage";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import {
|
|||||||
BottomSheetView,
|
BottomSheetView,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert, StyleSheet, TouchableOpacity, View } from "react-native";
|
import { Alert, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useDeletePlaylist } from "@/hooks/usePlaylistMutations";
|
import { useDeletePlaylist } from "@/hooks/usePlaylistMutations";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -25,6 +24,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useFavorite } from "@/hooks/useFavorite";
|
import { useFavorite } from "@/hooks/useFavorite";
|
||||||
import {
|
import {
|
||||||
audioStorageEvents,
|
audioStorageEvents,
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import type {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
BaseItemPerson,
|
BaseItemPerson,
|
||||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { router, useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||||
import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values";
|
import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
import { HorizontalScroll } from "../common/HorizontalScroll";
|
import { HorizontalScroll } from "../common/HorizontalScroll";
|
||||||
@@ -24,6 +25,7 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
const from = (segments as string[])[2];
|
const from = (segments as string[])[2];
|
||||||
|
|
||||||
const destinctPeople = useMemo(() => {
|
const destinctPeople = useMemo(() => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { router } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||||
import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values";
|
import { POSTER_CAROUSEL_HEIGHT } from "@/constants/Values";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
||||||
import { HorizontalScroll } from "../common/HorizontalScroll";
|
import { HorizontalScroll } from "../common/HorizontalScroll";
|
||||||
@@ -18,6 +18,7 @@ interface Props extends ViewProps {
|
|||||||
export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
|
export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
import { TouchableOpacity, View, type ViewProps } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
|||||||
@@ -54,29 +54,28 @@ export const SeasonDropdown: React.FC<Props> = ({
|
|||||||
[state, item, keys],
|
[state, item, keys],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Always use IndexNumber for Season objects (not keys.index which is for the item)
|
||||||
const sortByIndex = (a: BaseItemDto, b: BaseItemDto) =>
|
const sortByIndex = (a: BaseItemDto, b: BaseItemDto) =>
|
||||||
Number(a[keys.index]) - Number(b[keys.index]);
|
Number(a.IndexNumber) - Number(b.IndexNumber);
|
||||||
|
|
||||||
const optionGroups = useMemo(
|
const optionGroups = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
options:
|
options:
|
||||||
seasons?.sort(sortByIndex).map((season: any) => {
|
seasons?.sort(sortByIndex).map((season: any) => {
|
||||||
const title =
|
const title = season.Name || `Season ${season.IndexNumber}`;
|
||||||
season[keys.title] ||
|
|
||||||
season.Name ||
|
|
||||||
`Season ${season.IndexNumber}`;
|
|
||||||
return {
|
return {
|
||||||
type: "radio" as const,
|
type: "radio" as const,
|
||||||
label: title,
|
label: title,
|
||||||
value: season.Id || season.IndexNumber,
|
value: season.Id || season.IndexNumber,
|
||||||
selected: Number(season[keys.index]) === Number(seasonIndex),
|
// Compare season's IndexNumber with the selected seasonIndex
|
||||||
|
selected: Number(season.IndexNumber) === Number(seasonIndex),
|
||||||
onPress: () => onSelect(season),
|
onPress: () => onSelect(season),
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[seasons, keys, seasonIndex, onSelect],
|
[seasons, seasonIndex, onSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { router } from "expo-router";
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { TouchableOpacity, type ViewStyle } from "react-native";
|
import { TouchableOpacity, type ViewStyle } from "react-native";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
import { getDownloadedEpisodesBySeasonId } from "@/utils/downloads/offline-series";
|
||||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||||
import {
|
import {
|
||||||
HorizontalScroll,
|
HorizontalScroll,
|
||||||
@@ -17,7 +19,6 @@ import { ItemCardText } from "../ItemCardText";
|
|||||||
interface Props {
|
interface Props {
|
||||||
item?: BaseItemDto | null;
|
item?: BaseItemDto | null;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
isOffline?: boolean;
|
|
||||||
style?: ViewStyle;
|
style?: ViewStyle;
|
||||||
containerStyle?: ViewStyle;
|
containerStyle?: ViewStyle;
|
||||||
}
|
}
|
||||||
@@ -25,17 +26,14 @@ interface Props {
|
|||||||
export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
loading,
|
loading,
|
||||||
isOffline,
|
|
||||||
style,
|
style,
|
||||||
containerStyle,
|
containerStyle,
|
||||||
}) => {
|
}) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
|
const router = useRouter();
|
||||||
const { getDownloadedItems } = useDownload();
|
const { getDownloadedItems } = useDownload();
|
||||||
const downloadedFiles = useMemo(
|
|
||||||
() => getDownloadedItems(),
|
|
||||||
[getDownloadedItems],
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollRef = useRef<HorizontalScrollRef>(null);
|
const scrollRef = useRef<HorizontalScrollRef>(null);
|
||||||
|
|
||||||
@@ -51,11 +49,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
queryKey: ["episodes", seasonId, isOffline],
|
queryKey: ["episodes", seasonId, isOffline],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
return downloadedFiles
|
return getDownloadedEpisodesBySeasonId(getDownloadedItems(), seasonId!);
|
||||||
?.filter(
|
|
||||||
(f) => f.item.Type === "Episode" && f.item.SeasonId === seasonId,
|
|
||||||
)
|
|
||||||
.map((f) => f.item);
|
|
||||||
}
|
}
|
||||||
if (!api || !user?.Id || !item?.SeriesId) return [];
|
if (!api || !user?.Id || !item?.SeriesId) return [];
|
||||||
const response = await getTvShowsApi(api).getEpisodes({
|
const response = await getTvShowsApi(api).getEpisodes({
|
||||||
@@ -73,7 +67,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
return response.data.Items as BaseItemDto[];
|
return response.data.Items as BaseItemDto[];
|
||||||
},
|
},
|
||||||
enabled: !!api && !!user?.Id && !!seasonId,
|
enabled: !!seasonId && (isOffline || (!!api && !!user?.Id)),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ import {
|
|||||||
SeasonDropdown,
|
SeasonDropdown,
|
||||||
type SeasonIndexState,
|
type SeasonIndexState,
|
||||||
} from "@/components/series/SeasonDropdown";
|
} from "@/components/series/SeasonDropdown";
|
||||||
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
import {
|
||||||
|
buildOfflineSeasons,
|
||||||
|
getDownloadedEpisodesForSeason,
|
||||||
|
} from "@/utils/downloads/offline-series";
|
||||||
import { runtimeTicksToSeconds } from "@/utils/time";
|
import { runtimeTicksToSeconds } from "@/utils/time";
|
||||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
@@ -31,6 +37,8 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
|
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
|
const { getDownloadedItems, downloadedItems } = useDownload();
|
||||||
|
|
||||||
const seasonIndex = useMemo(
|
const seasonIndex = useMemo(
|
||||||
() => seasonIndexState[item.Id ?? ""],
|
() => seasonIndexState[item.Id ?? ""],
|
||||||
@@ -38,8 +46,12 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: seasons } = useQuery({
|
const { data: seasons } = useQuery({
|
||||||
queryKey: ["seasons", item.Id],
|
queryKey: ["seasons", item.Id, isOffline, downloadedItems.length],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if (isOffline) {
|
||||||
|
return buildOfflineSeasons(getDownloadedItems(), item.Id!);
|
||||||
|
}
|
||||||
|
|
||||||
if (!api || !user?.Id || !item.Id) return [];
|
if (!api || !user?.Id || !item.Id) return [];
|
||||||
const response = await api.axiosInstance.get(
|
const response = await api.axiosInstance.get(
|
||||||
`${api.basePath}/Shows/${item.Id}/Seasons`,
|
`${api.basePath}/Shows/${item.Id}/Seasons`,
|
||||||
@@ -58,8 +70,8 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
|
|
||||||
return response.data.Items;
|
return response.data.Items;
|
||||||
},
|
},
|
||||||
staleTime: 60,
|
staleTime: isOffline ? Infinity : 60,
|
||||||
enabled: !!api && !!user?.Id && !!item.Id,
|
enabled: isOffline || (!!api && !!user?.Id && !!item.Id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedSeasonId: string | null = useMemo(() => {
|
const selectedSeasonId: string | null = useMemo(() => {
|
||||||
@@ -73,9 +85,33 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
return season.Id!;
|
return season.Id!;
|
||||||
}, [seasons, seasonIndex]);
|
}, [seasons, seasonIndex]);
|
||||||
|
|
||||||
|
// For offline mode, we use season index number instead of ID
|
||||||
|
const selectedSeasonNumber = useMemo(() => {
|
||||||
|
if (!isOffline) return null;
|
||||||
|
const season = seasons?.find(
|
||||||
|
(s: BaseItemDto) =>
|
||||||
|
s.IndexNumber === seasonIndex || s.Name === seasonIndex,
|
||||||
|
);
|
||||||
|
return season?.IndexNumber ?? null;
|
||||||
|
}, [isOffline, seasons, seasonIndex]);
|
||||||
|
|
||||||
const { data: episodes, isPending } = useQuery({
|
const { data: episodes, isPending } = useQuery({
|
||||||
queryKey: ["episodes", item.Id, selectedSeasonId],
|
queryKey: [
|
||||||
|
"episodes",
|
||||||
|
item.Id,
|
||||||
|
isOffline ? selectedSeasonNumber : selectedSeasonId,
|
||||||
|
isOffline,
|
||||||
|
downloadedItems.length,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if (isOffline) {
|
||||||
|
return getDownloadedEpisodesForSeason(
|
||||||
|
getDownloadedItems(),
|
||||||
|
item.Id!,
|
||||||
|
selectedSeasonNumber!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!api || !user?.Id || !item.Id || !selectedSeasonId) {
|
if (!api || !user?.Id || !item.Id || !selectedSeasonId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -85,7 +121,6 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
seasonId: selectedSeasonId,
|
seasonId: selectedSeasonId,
|
||||||
enableUserData: true,
|
enableUserData: true,
|
||||||
// Note: Including trick play is necessary to enable trick play downloads
|
|
||||||
fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
|
fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,7 +132,10 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
|
|
||||||
return res.data.Items;
|
return res.data.Items;
|
||||||
},
|
},
|
||||||
enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId,
|
staleTime: isOffline ? Infinity : 0,
|
||||||
|
enabled: isOffline
|
||||||
|
? !!item.Id && selectedSeasonNumber !== null
|
||||||
|
: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Used for height calculation
|
// Used for height calculation
|
||||||
@@ -127,7 +165,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{episodes?.length ? (
|
{episodes?.length && !isOffline ? (
|
||||||
<View className='flex flex-row items-center space-x-2'>
|
<View className='flex flex-row items-center space-x-2'>
|
||||||
<DownloadItems
|
<DownloadItems
|
||||||
title={t("item_card.download.download_season")}
|
title={t("item_card.download.download_season")}
|
||||||
@@ -180,9 +218,11 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
{runtimeTicksToSeconds(e.RunTimeTicks)}
|
{runtimeTicksToSeconds(e.RunTimeTicks)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className='self-start ml-auto -mt-0.5'>
|
{!isOffline && (
|
||||||
<DownloadSingleItem item={e} />
|
<View className='self-start ml-auto -mt-0.5'>
|
||||||
</View>
|
<DownloadSingleItem item={e} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useRouter } from "expo-router";
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Linking, Switch } from "react-native";
|
import { Linking, Switch } from "react-native";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
|
import { useSessions, type useSessionsProps } from "@/hooks/useSessions";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
|||||||
224
components/settings/LocalNetworkSettings.tsx
Normal file
224
components/settings/LocalNetworkSettings.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import type React from "react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Switch, TouchableOpacity, View } from "react-native";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { useWifiSSID } from "@/hooks/useWifiSSID";
|
||||||
|
import { useServerUrl } from "@/providers/ServerUrlProvider";
|
||||||
|
import { storage } from "@/utils/mmkv";
|
||||||
|
import {
|
||||||
|
getServerLocalConfig,
|
||||||
|
type LocalNetworkConfig,
|
||||||
|
updateServerLocalConfig,
|
||||||
|
} from "@/utils/secureCredentials";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import { Input } from "../common/Input";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: LocalNetworkConfig = {
|
||||||
|
localUrl: "",
|
||||||
|
homeWifiSSIDs: [],
|
||||||
|
enabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StatusDisplayProps {
|
||||||
|
currentSSID: string | null;
|
||||||
|
isUsingLocalUrl: boolean;
|
||||||
|
t: (key: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusDisplay({
|
||||||
|
currentSSID,
|
||||||
|
isUsingLocalUrl,
|
||||||
|
t,
|
||||||
|
}: StatusDisplayProps): React.ReactElement {
|
||||||
|
const wifiStatus = currentSSID ?? t("home.settings.network.not_connected");
|
||||||
|
const urlType = isUsingLocalUrl
|
||||||
|
? t("home.settings.network.local")
|
||||||
|
: t("home.settings.network.remote");
|
||||||
|
const urlTypeColor = isUsingLocalUrl ? "text-green-500" : "text-blue-500";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='px-4 py-2 bg-neutral-900 rounded-xl mt-4'>
|
||||||
|
<View className='flex-row justify-between items-center py-1'>
|
||||||
|
<Text className='text-neutral-400'>
|
||||||
|
{t("home.settings.network.current_wifi")}
|
||||||
|
</Text>
|
||||||
|
<Text>{wifiStatus}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='flex-row justify-between items-center py-1'>
|
||||||
|
<Text className='text-neutral-400'>
|
||||||
|
{t("home.settings.network.using_url")}
|
||||||
|
</Text>
|
||||||
|
<Text className={urlTypeColor}>{urlType}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalNetworkSettings(): React.ReactElement | null {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { permissionStatus, requestPermission } = useWifiSSID();
|
||||||
|
const { isUsingLocalUrl, currentSSID, refreshUrlState } = useServerUrl();
|
||||||
|
|
||||||
|
const remoteUrl = storage.getString("serverUrl");
|
||||||
|
const [config, setConfig] = useState<LocalNetworkConfig>(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (remoteUrl) {
|
||||||
|
const existingConfig = getServerLocalConfig(remoteUrl);
|
||||||
|
if (existingConfig) {
|
||||||
|
setConfig(existingConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [remoteUrl]);
|
||||||
|
|
||||||
|
const saveConfig = useCallback(
|
||||||
|
(newConfig: LocalNetworkConfig) => {
|
||||||
|
if (!remoteUrl) return;
|
||||||
|
setConfig(newConfig);
|
||||||
|
updateServerLocalConfig(remoteUrl, newConfig);
|
||||||
|
// Trigger URL re-evaluation after config change
|
||||||
|
refreshUrlState();
|
||||||
|
},
|
||||||
|
[remoteUrl, refreshUrlState],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleEnabled = useCallback(
|
||||||
|
async (enabled: boolean) => {
|
||||||
|
if (enabled && permissionStatus !== "granted") {
|
||||||
|
const granted = await requestPermission();
|
||||||
|
if (!granted) {
|
||||||
|
toast.error(t("home.settings.network.permission_denied"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveConfig({ ...config, enabled });
|
||||||
|
},
|
||||||
|
[config, permissionStatus, requestPermission, saveConfig, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLocalUrlChange = useCallback(
|
||||||
|
(localUrl: string) => {
|
||||||
|
saveConfig({ ...config, localUrl });
|
||||||
|
},
|
||||||
|
[config, saveConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddCurrentNetwork = useCallback(() => {
|
||||||
|
if (!currentSSID) {
|
||||||
|
toast.error(t("home.settings.network.no_wifi_connected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.homeWifiSSIDs.includes(currentSSID)) {
|
||||||
|
toast.info(t("home.settings.network.network_already_added"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveConfig({
|
||||||
|
...config,
|
||||||
|
homeWifiSSIDs: [...config.homeWifiSSIDs, currentSSID],
|
||||||
|
});
|
||||||
|
toast.success(t("home.settings.network.network_added"));
|
||||||
|
}, [config, currentSSID, saveConfig, t]);
|
||||||
|
|
||||||
|
const handleRemoveNetwork = useCallback(
|
||||||
|
(ssidToRemove: string) => {
|
||||||
|
saveConfig({
|
||||||
|
...config,
|
||||||
|
homeWifiSSIDs: config.homeWifiSSIDs.filter((s) => s !== ssidToRemove),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[config, saveConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!remoteUrl) return null;
|
||||||
|
|
||||||
|
const addNetworkButtonText = currentSSID
|
||||||
|
? t("home.settings.network.add_current_network", { ssid: currentSSID })
|
||||||
|
: t("home.settings.network.not_connected_to_wifi");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ListGroup title={t("home.settings.network.local_network")}>
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.network.auto_switch_enabled")}
|
||||||
|
subtitle={t("home.settings.network.auto_switch_description")}
|
||||||
|
>
|
||||||
|
<Switch value={config.enabled} onValueChange={handleToggleEnabled} />
|
||||||
|
</ListItem>
|
||||||
|
</ListGroup>
|
||||||
|
|
||||||
|
{config.enabled && (
|
||||||
|
<View className='pt-4'>
|
||||||
|
<ListGroup
|
||||||
|
title={t("home.settings.network.local_url")}
|
||||||
|
description={
|
||||||
|
<Text className='text-[#8E8D91] text-xs'>
|
||||||
|
{t("home.settings.network.local_url_hint")}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View className=''>
|
||||||
|
<Input
|
||||||
|
placeholder={t("home.settings.network.local_url_placeholder")}
|
||||||
|
value={config.localUrl}
|
||||||
|
onChangeText={handleLocalUrlChange}
|
||||||
|
keyboardType='url'
|
||||||
|
autoCapitalize='none'
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ListGroup>
|
||||||
|
|
||||||
|
<ListGroup
|
||||||
|
title={t("home.settings.network.home_wifi_networks")}
|
||||||
|
className='mt-4'
|
||||||
|
>
|
||||||
|
{config.homeWifiSSIDs.map((wifiSSID) => (
|
||||||
|
<ListItem key={wifiSSID} title={wifiSSID}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleRemoveNetwork(wifiSSID)}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
>
|
||||||
|
<Ionicons name='close-circle' size={22} color='#EF4444' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
{config.homeWifiSSIDs.length === 0 && (
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.network.no_networks_configured")}
|
||||||
|
subtitle={t("home.settings.network.add_network_hint")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ListGroup>
|
||||||
|
|
||||||
|
<View className='py-2'>
|
||||||
|
<Button
|
||||||
|
onPress={handleAddCurrentNetwork}
|
||||||
|
disabled={!currentSSID || permissionStatus !== "granted"}
|
||||||
|
>
|
||||||
|
{addNetworkButtonText}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<StatusDisplay
|
||||||
|
currentSSID={currentSSID}
|
||||||
|
isUsingLocalUrl={isUsingLocalUrl}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissionStatus === "denied" && (
|
||||||
|
<View className='py-2'>
|
||||||
|
<Text className='text-xs text-red-500'>
|
||||||
|
{t("home.settings.network.permission_denied_explanation")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@@ -8,6 +7,7 @@ import { Linking, Switch, View } from "react-native";
|
|||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
import { PlatformDropdown } from "@/components/PlatformDropdown";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
const orientations = [
|
const orientations = [
|
||||||
ScreenOrientation.OrientationLock.DEFAULT,
|
ScreenOrientation.OrientationLock.DEFAULT,
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||||
|
ScreenOrientation.OrientationLock.LANDSCAPE,
|
||||||
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
|
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
|
||||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
|
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
|
||||||
];
|
];
|
||||||
@@ -39,6 +40,8 @@ export const PlaybackControlsSettings: React.FC = () => {
|
|||||||
"home.settings.other.orientations.DEFAULT",
|
"home.settings.other.orientations.DEFAULT",
|
||||||
[ScreenOrientation.OrientationLock.PORTRAIT_UP]:
|
[ScreenOrientation.OrientationLock.PORTRAIT_UP]:
|
||||||
"home.settings.other.orientations.PORTRAIT_UP",
|
"home.settings.other.orientations.PORTRAIT_UP",
|
||||||
|
[ScreenOrientation.OrientationLock.LANDSCAPE]:
|
||||||
|
"home.settings.other.orientations.LANDSCAPE",
|
||||||
[ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]:
|
[ScreenOrientation.OrientationLock.LANDSCAPE_LEFT]:
|
||||||
"home.settings.other.orientations.LANDSCAPE_LEFT",
|
"home.settings.other.orientations.LANDSCAPE_LEFT",
|
||||||
[ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]:
|
[ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT]:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
import { ListItem } from "../list/ListItem";
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useRouter } from "expo-router";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
|
||||||
export interface ContinueWatchingOverlayProps {
|
export interface ContinueWatchingOverlayProps {
|
||||||
@@ -23,7 +23,7 @@ const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
|
|||||||
settings.maxAutoPlayEpisodeCount.value ? (
|
settings.maxAutoPlayEpisodeCount.value ? (
|
||||||
<View
|
<View
|
||||||
className={
|
className={
|
||||||
"absolute top-0 bottom-0 left-0 right-0 flex flex-col px-4 items-center justify-center bg-[#000000B3]"
|
"absolute top-0 bottom-0 left-0 right-0 z-50 flex flex-col px-4 items-center justify-center bg-[#000000B3]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text className='text-2xl font-bold text-white py-4 '>
|
<Text className='text-2xl font-bold text-white py-4 '>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { type FC, useCallback, useEffect, useState } from "react";
|
import { type FC, useCallback, useEffect, useState } from "react";
|
||||||
import { StyleSheet, useWindowDimensions, View } from "react-native";
|
import { StyleSheet, useWindowDimensions, View } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -15,12 +15,15 @@ import Animated, {
|
|||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
|
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
||||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||||
|
import type { TechnicalInfo } from "@/modules/mpv-player";
|
||||||
import { DownloadedItem } from "@/providers/Downloads/types";
|
import { DownloadedItem } from "@/providers/Downloads/types";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
import { ticksToMs } from "@/utils/time";
|
import { ticksToMs } from "@/utils/time";
|
||||||
@@ -34,6 +37,7 @@ import { useRemoteControl } from "./hooks/useRemoteControl";
|
|||||||
import { useVideoNavigation } from "./hooks/useVideoNavigation";
|
import { useVideoNavigation } from "./hooks/useVideoNavigation";
|
||||||
import { useVideoSlider } from "./hooks/useVideoSlider";
|
import { useVideoSlider } from "./hooks/useVideoSlider";
|
||||||
import { useVideoTime } from "./hooks/useVideoTime";
|
import { useVideoTime } from "./hooks/useVideoTime";
|
||||||
|
import { TechnicalInfoOverlay } from "./TechnicalInfoOverlay";
|
||||||
import { useControlsTimeout } from "./useControlsTimeout";
|
import { useControlsTimeout } from "./useControlsTimeout";
|
||||||
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
||||||
import { type AspectRatio } from "./VideoScalingModeSelector";
|
import { type AspectRatio } from "./VideoScalingModeSelector";
|
||||||
@@ -49,7 +53,6 @@ interface Props {
|
|||||||
enableTrickplay?: boolean;
|
enableTrickplay?: boolean;
|
||||||
togglePlay: () => void;
|
togglePlay: () => void;
|
||||||
setShowControls: (shown: boolean) => void;
|
setShowControls: (shown: boolean) => void;
|
||||||
offline?: boolean;
|
|
||||||
mediaSource?: MediaSourceInfo | null;
|
mediaSource?: MediaSourceInfo | null;
|
||||||
seek: (ticks: number) => void;
|
seek: (ticks: number) => void;
|
||||||
startPictureInPicture?: () => Promise<void>;
|
startPictureInPicture?: () => Promise<void>;
|
||||||
@@ -63,6 +66,12 @@ interface Props {
|
|||||||
// Playback speed props
|
// Playback speed props
|
||||||
playbackSpeed?: number;
|
playbackSpeed?: number;
|
||||||
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
||||||
|
// Technical info props
|
||||||
|
showTechnicalInfo?: boolean;
|
||||||
|
onToggleTechnicalInfo?: () => void;
|
||||||
|
getTechnicalInfo?: () => Promise<TechnicalInfo>;
|
||||||
|
playMethod?: "DirectPlay" | "DirectStream" | "Transcode";
|
||||||
|
transcodeReasons?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Controls: FC<Props> = ({
|
export const Controls: FC<Props> = ({
|
||||||
@@ -83,12 +92,17 @@ export const Controls: FC<Props> = ({
|
|||||||
aspectRatio = "default",
|
aspectRatio = "default",
|
||||||
isZoomedToFill = false,
|
isZoomedToFill = false,
|
||||||
onZoomToggle,
|
onZoomToggle,
|
||||||
offline = false,
|
|
||||||
api = null,
|
api = null,
|
||||||
downloadedFiles = undefined,
|
downloadedFiles = undefined,
|
||||||
playbackSpeed = 1.0,
|
playbackSpeed = 1.0,
|
||||||
setPlaybackSpeed,
|
setPlaybackSpeed,
|
||||||
|
showTechnicalInfo = false,
|
||||||
|
onToggleTechnicalInfo,
|
||||||
|
getTechnicalInfo,
|
||||||
|
playMethod,
|
||||||
|
transcodeReasons,
|
||||||
}) => {
|
}) => {
|
||||||
|
const offline = useOfflineMode();
|
||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
@@ -110,7 +124,9 @@ export const Controls: FC<Props> = ({
|
|||||||
} = useTrickplay(item);
|
} = useTrickplay(item);
|
||||||
|
|
||||||
const min = useSharedValue(0);
|
const min = useSharedValue(0);
|
||||||
const max = useSharedValue(ticksToMs(item.RunTimeTicks || 0));
|
// Regular value for use during render (avoids Reanimated warning)
|
||||||
|
const maxMs = ticksToMs(item.RunTimeTicks || 0);
|
||||||
|
const max = useSharedValue(maxMs);
|
||||||
|
|
||||||
// Animation values for controls
|
// Animation values for controls
|
||||||
const controlsOpacity = useSharedValue(showControls ? 1 : 0);
|
const controlsOpacity = useSharedValue(showControls ? 1 : 0);
|
||||||
@@ -303,7 +319,7 @@ export const Controls: FC<Props> = ({
|
|||||||
offline,
|
offline,
|
||||||
api,
|
api,
|
||||||
downloadedFiles,
|
downloadedFiles,
|
||||||
max.value,
|
maxMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToItemCommon = useCallback(
|
const goToItemCommon = useCallback(
|
||||||
@@ -457,6 +473,16 @@ export const Controls: FC<Props> = ({
|
|||||||
onSkipForward={handleSkipForward}
|
onSkipForward={handleSkipForward}
|
||||||
onSkipBackward={handleSkipBackward}
|
onSkipBackward={handleSkipBackward}
|
||||||
/>
|
/>
|
||||||
|
{/* Technical Info Overlay - rendered outside animated views to stay visible */}
|
||||||
|
{getTechnicalInfo && (
|
||||||
|
<TechnicalInfoOverlay
|
||||||
|
showControls={showControls}
|
||||||
|
visible={showTechnicalInfo}
|
||||||
|
getTechnicalInfo={getTechnicalInfo}
|
||||||
|
playMethod={playMethod}
|
||||||
|
transcodeReasons={transcodeReasons}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={headerAnimatedStyle}
|
style={headerAnimatedStyle}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
@@ -477,6 +503,8 @@ export const Controls: FC<Props> = ({
|
|||||||
onZoomToggle={onZoomToggle}
|
onZoomToggle={onZoomToggle}
|
||||||
playbackSpeed={playbackSpeed}
|
playbackSpeed={playbackSpeed}
|
||||||
setPlaybackSpeed={setPlaybackSpeed}
|
setPlaybackSpeed={setPlaybackSpeed}
|
||||||
|
showTechnicalInfo={showTechnicalInfo}
|
||||||
|
onToggleTechnicalInfo={onToggleTechnicalInfo}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useGlobalSearchParams } from "expo-router";
|
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
||||||
import {
|
import {
|
||||||
HorizontalScroll,
|
HorizontalScroll,
|
||||||
@@ -19,10 +18,16 @@ import {
|
|||||||
type SeasonIndexState,
|
type SeasonIndexState,
|
||||||
} from "@/components/series/SeasonDropdown";
|
} from "@/components/series/SeasonDropdown";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
import type { DownloadedItem } from "@/providers/Downloads/types";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import {
|
||||||
|
getDownloadedEpisodesForSeason,
|
||||||
|
getDownloadedSeasonNumbers,
|
||||||
|
} from "@/utils/downloads/offline-series";
|
||||||
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||||
import { runtimeTicksToSeconds } from "@/utils/time";
|
import { runtimeTicksToSeconds } from "@/utils/time";
|
||||||
|
import { HEADER_LAYOUT, ICON_SIZES } from "./constants";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -40,10 +45,9 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
const scrollToIndex = (index: number) => {
|
const scrollToIndex = (index: number) => {
|
||||||
scrollViewRef.current?.scrollToIndex(index, 100);
|
scrollViewRef.current?.scrollToIndex(index, 100);
|
||||||
};
|
};
|
||||||
const { offline } = useGlobalSearchParams<{
|
const isOffline = useOfflineMode();
|
||||||
offline: string;
|
const { settings } = useSettings();
|
||||||
}>();
|
const insets = useSafeAreaInsets();
|
||||||
const isOffline = offline === "true";
|
|
||||||
|
|
||||||
// Set the initial season index
|
// Set the initial season index
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -56,10 +60,6 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { getDownloadedItems } = useDownload();
|
const { getDownloadedItems } = useDownload();
|
||||||
const downloadedFiles = useMemo(
|
|
||||||
() => getDownloadedItems(),
|
|
||||||
[getDownloadedItems],
|
|
||||||
);
|
|
||||||
|
|
||||||
const seasonIndex = seasonIndexState[item.ParentId ?? ""];
|
const seasonIndex = seasonIndexState[item.ParentId ?? ""];
|
||||||
|
|
||||||
@@ -68,15 +68,9 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
if (!item.SeriesId) return [];
|
if (!item.SeriesId) return [];
|
||||||
const seriesEpisodes = downloadedFiles?.filter(
|
const seasonNumbers = getDownloadedSeasonNumbers(
|
||||||
(f: DownloadedItem) => f.item.SeriesId === item.SeriesId,
|
getDownloadedItems(),
|
||||||
);
|
item.SeriesId,
|
||||||
const seasonNumbers = Array.from(
|
|
||||||
new Set(
|
|
||||||
seriesEpisodes
|
|
||||||
?.map((f: DownloadedItem) => f.item.ParentIndexNumber)
|
|
||||||
.filter(Boolean),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
// Create fake season objects
|
// Create fake season objects
|
||||||
return seasonNumbers.map((seasonNumber) => ({
|
return seasonNumbers.map((seasonNumber) => ({
|
||||||
@@ -117,14 +111,12 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
queryKey: ["episodes", item.SeriesId, selectedSeasonId],
|
queryKey: ["episodes", item.SeriesId, selectedSeasonId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
if (!item.SeriesId) return [];
|
if (!item.SeriesId || typeof seasonIndex !== "number") return [];
|
||||||
return downloadedFiles
|
return getDownloadedEpisodesForSeason(
|
||||||
?.filter(
|
getDownloadedItems(),
|
||||||
(f: DownloadedItem) =>
|
item.SeriesId,
|
||||||
f.item.SeriesId === item.SeriesId &&
|
seasonIndex,
|
||||||
f.item.ParentIndexNumber === seasonIndex,
|
);
|
||||||
)
|
|
||||||
.map((f: DownloadedItem) => f.item);
|
|
||||||
}
|
}
|
||||||
if (!api || !user?.Id || !item.Id || !selectedSeasonId) return [];
|
if (!api || !user?.Id || !item.Id || !selectedSeasonId) return [];
|
||||||
const res = await getTvShowsApi(api).getEpisodes({
|
const res = await getTvShowsApi(api).getEpisodes({
|
||||||
@@ -153,6 +145,9 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Don't prefetch when offline - data is already local
|
||||||
|
if (isOffline) return;
|
||||||
|
|
||||||
for (const e of episodes || []) {
|
for (const e of episodes || []) {
|
||||||
queryClient.prefetchQuery({
|
queryClient.prefetchQuery({
|
||||||
queryKey: ["item", e.Id],
|
queryKey: ["item", e.Id],
|
||||||
@@ -168,7 +163,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
staleTime: 60 * 5 * 1000,
|
staleTime: 60 * 5 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [episodes]);
|
}, [episodes, isOffline]);
|
||||||
|
|
||||||
// Scroll to the current item when episodes are fetched
|
// Scroll to the current item when episodes are fetched
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -181,15 +176,24 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
}, [episodes, item.Id]);
|
}, [episodes, item.Id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
paddingTop:
|
||||||
|
(settings?.safeAreaInControlsEnabled ?? true) ? insets.top : 0,
|
||||||
|
paddingLeft:
|
||||||
|
(settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
||||||
|
paddingRight:
|
||||||
|
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className='flex-row items-center p-4 z-10'>
|
<View
|
||||||
|
style={{ padding: HEADER_LAYOUT.CONTAINER_PADDING }}
|
||||||
|
className='flex-row items-center z-10'
|
||||||
|
>
|
||||||
{seasons && seasons.length > 0 && !episodesLoading && episodes && (
|
{seasons && seasons.length > 0 && !episodesLoading && episodes && (
|
||||||
<SeasonDropdown
|
<SeasonDropdown
|
||||||
item={item}
|
item={item}
|
||||||
@@ -207,9 +211,9 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2 ml-auto'
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2 ml-auto'
|
||||||
>
|
>
|
||||||
<Ionicons name='close' size={24} color='white' />
|
<Ionicons name='close' size={ICON_SIZES.HEADER} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -274,6 +278,6 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { type FC, useCallback, useState } from "react";
|
import { type FC, useCallback, useState } from "react";
|
||||||
import { Platform, TouchableOpacity, View } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import { OrientationLock } from "@/packages/expo-screen-orientation";
|
import { OrientationLock } from "@/packages/expo-screen-orientation";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { ICON_SIZES } from "./constants";
|
import { HEADER_LAYOUT, ICON_SIZES } from "./constants";
|
||||||
import DropdownView from "./dropdown/DropdownView";
|
import DropdownView from "./dropdown/DropdownView";
|
||||||
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
import { PlaybackSpeedScope } from "./utils/playback-speed-settings";
|
||||||
import { type AspectRatio } from "./VideoScalingModeSelector";
|
import { type AspectRatio } from "./VideoScalingModeSelector";
|
||||||
@@ -34,6 +34,9 @@ interface HeaderControlsProps {
|
|||||||
// Playback speed props
|
// Playback speed props
|
||||||
playbackSpeed?: number;
|
playbackSpeed?: number;
|
||||||
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
||||||
|
// Technical info props
|
||||||
|
showTechnicalInfo?: boolean;
|
||||||
|
onToggleTechnicalInfo?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderControls: FC<HeaderControlsProps> = ({
|
export const HeaderControls: FC<HeaderControlsProps> = ({
|
||||||
@@ -52,6 +55,8 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
|||||||
onZoomToggle,
|
onZoomToggle,
|
||||||
playbackSpeed = 1.0,
|
playbackSpeed = 1.0,
|
||||||
setPlaybackSpeed,
|
setPlaybackSpeed,
|
||||||
|
showTechnicalInfo = false,
|
||||||
|
onToggleTechnicalInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -98,17 +103,20 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
|
|||||||
left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
left: (settings?.safeAreaInControlsEnabled ?? true) ? insets.left : 0,
|
||||||
right:
|
right:
|
||||||
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
(settings?.safeAreaInControlsEnabled ?? true) ? insets.right : 0,
|
||||||
|
padding: HEADER_LAYOUT.CONTAINER_PADDING,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
className='flex flex-row justify-between'
|
className='flex flex-row justify-between'
|
||||||
>
|
>
|
||||||
<View className='mr-auto p-2' pointerEvents='box-none'>
|
<View className='mr-auto' pointerEvents='box-none'>
|
||||||
{!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && (
|
{!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && (
|
||||||
<View pointerEvents='auto'>
|
<View pointerEvents='auto'>
|
||||||
<DropdownView
|
<DropdownView
|
||||||
playbackSpeed={playbackSpeed}
|
playbackSpeed={playbackSpeed}
|
||||||
setPlaybackSpeed={setPlaybackSpeed}
|
setPlaybackSpeed={setPlaybackSpeed}
|
||||||
|
showTechnicalInfo={showTechnicalInfo}
|
||||||
|
onToggleTechnicalInfo={onToggleTechnicalInfo}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
283
components/video-player/controls/TechnicalInfoOverlay.tsx
Normal file
283
components/video-player/controls/TechnicalInfoOverlay.tsx
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { type FC, memo, useCallback, useEffect, useState } from "react";
|
||||||
|
import { Platform, StyleSheet, Text, View } from "react-native";
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import type { TechnicalInfo } from "@/modules/mpv-player";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { HEADER_LAYOUT } from "./constants";
|
||||||
|
|
||||||
|
type PlayMethod = "DirectPlay" | "DirectStream" | "Transcode";
|
||||||
|
|
||||||
|
interface TechnicalInfoOverlayProps {
|
||||||
|
showControls: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
getTechnicalInfo: () => Promise<TechnicalInfo>;
|
||||||
|
playMethod?: PlayMethod;
|
||||||
|
transcodeReasons?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatBitrate = (bitsPerSecond: number): string => {
|
||||||
|
const mbps = bitsPerSecond / 1_000_000;
|
||||||
|
if (mbps >= 1) {
|
||||||
|
return `${mbps.toFixed(1)} Mbps`;
|
||||||
|
}
|
||||||
|
const kbps = bitsPerSecond / 1_000;
|
||||||
|
return `${kbps.toFixed(0)} Kbps`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCodec = (codec: string): string => {
|
||||||
|
// Normalize common codec names
|
||||||
|
const codecMap: Record<string, string> = {
|
||||||
|
h264: "H.264",
|
||||||
|
hevc: "HEVC",
|
||||||
|
h265: "HEVC",
|
||||||
|
vp9: "VP9",
|
||||||
|
vp8: "VP8",
|
||||||
|
av1: "AV1",
|
||||||
|
aac: "AAC",
|
||||||
|
ac3: "AC3",
|
||||||
|
eac3: "E-AC3",
|
||||||
|
dts: "DTS",
|
||||||
|
truehd: "TrueHD",
|
||||||
|
flac: "FLAC",
|
||||||
|
opus: "Opus",
|
||||||
|
mp3: "MP3",
|
||||||
|
};
|
||||||
|
return codecMap[codec.toLowerCase()] || codec.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFps = (fps: number): string => {
|
||||||
|
// Common frame rates
|
||||||
|
if (Math.abs(fps - 23.976) < 0.01) return "23.976";
|
||||||
|
if (Math.abs(fps - 29.97) < 0.01) return "29.97";
|
||||||
|
if (Math.abs(fps - 59.94) < 0.01) return "59.94";
|
||||||
|
if (Number.isInteger(fps)) return fps.toString();
|
||||||
|
return fps.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlayMethodLabel = (method: PlayMethod): string => {
|
||||||
|
switch (method) {
|
||||||
|
case "DirectPlay":
|
||||||
|
return "Direct Play";
|
||||||
|
case "DirectStream":
|
||||||
|
return "Direct Stream";
|
||||||
|
case "Transcode":
|
||||||
|
return "Transcoding";
|
||||||
|
default:
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlayMethodColor = (method: PlayMethod): string => {
|
||||||
|
switch (method) {
|
||||||
|
case "DirectPlay":
|
||||||
|
return "#4ade80"; // green
|
||||||
|
case "DirectStream":
|
||||||
|
return "#60a5fa"; // blue
|
||||||
|
case "Transcode":
|
||||||
|
return "#fbbf24"; // yellow/amber
|
||||||
|
default:
|
||||||
|
return "white";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTranscodeReason = (reason: string): string => {
|
||||||
|
// Convert camelCase/PascalCase to readable format
|
||||||
|
const reasonMap: Record<string, string> = {
|
||||||
|
ContainerNotSupported: "Container not supported",
|
||||||
|
VideoCodecNotSupported: "Video codec not supported",
|
||||||
|
AudioCodecNotSupported: "Audio codec not supported",
|
||||||
|
SubtitleCodecNotSupported: "Subtitle codec not supported",
|
||||||
|
AudioIsExternal: "Audio is external",
|
||||||
|
SecondaryAudioNotSupported: "Secondary audio not supported",
|
||||||
|
VideoProfileNotSupported: "Video profile not supported",
|
||||||
|
VideoLevelNotSupported: "Video level not supported",
|
||||||
|
VideoResolutionNotSupported: "Resolution not supported",
|
||||||
|
VideoBitDepthNotSupported: "Bit depth not supported",
|
||||||
|
VideoFramerateNotSupported: "Framerate not supported",
|
||||||
|
RefFramesNotSupported: "Ref frames not supported",
|
||||||
|
AnamorphicVideoNotSupported: "Anamorphic video not supported",
|
||||||
|
InterlacedVideoNotSupported: "Interlaced video not supported",
|
||||||
|
AudioChannelsNotSupported: "Audio channels not supported",
|
||||||
|
AudioProfileNotSupported: "Audio profile not supported",
|
||||||
|
AudioSampleRateNotSupported: "Sample rate not supported",
|
||||||
|
AudioBitDepthNotSupported: "Audio bit depth not supported",
|
||||||
|
ContainerBitrateExceedsLimit: "Bitrate exceeds limit",
|
||||||
|
VideoBitrateNotSupported: "Video bitrate not supported",
|
||||||
|
AudioBitrateNotSupported: "Audio bitrate not supported",
|
||||||
|
UnknownVideoStreamInfo: "Unknown video stream",
|
||||||
|
UnknownAudioStreamInfo: "Unknown audio stream",
|
||||||
|
DirectPlayError: "Direct play error",
|
||||||
|
VideoRangeTypeNotSupported: "HDR not supported",
|
||||||
|
VideoCodecTagNotSupported: "Video codec tag not supported",
|
||||||
|
};
|
||||||
|
return reasonMap[reason] || reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
|
||||||
|
({
|
||||||
|
showControls,
|
||||||
|
visible,
|
||||||
|
getTechnicalInfo,
|
||||||
|
playMethod,
|
||||||
|
transcodeReasons,
|
||||||
|
}) => {
|
||||||
|
const { settings } = useSettings();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const [info, setInfo] = useState<TechnicalInfo | null>(null);
|
||||||
|
|
||||||
|
const opacity = useSharedValue(0);
|
||||||
|
|
||||||
|
// Animate visibility based on visible prop only (stays visible regardless of controls)
|
||||||
|
useEffect(() => {
|
||||||
|
opacity.value = withTiming(visible ? 1 : 0, {
|
||||||
|
duration: 300,
|
||||||
|
easing: Easing.out(Easing.quad),
|
||||||
|
});
|
||||||
|
}, [visible, opacity]);
|
||||||
|
|
||||||
|
// Fetch technical info periodically when visible
|
||||||
|
const fetchInfo = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const data = await getTechnicalInfo();
|
||||||
|
setInfo(data);
|
||||||
|
} catch (_error) {
|
||||||
|
// Silently fail - the info is optional
|
||||||
|
}
|
||||||
|
}, [getTechnicalInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch immediately
|
||||||
|
fetchInfo();
|
||||||
|
|
||||||
|
// Then fetch every 2 seconds
|
||||||
|
const interval = setInterval(fetchInfo, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [visible, fetchInfo]);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: opacity.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Hide on TV platforms
|
||||||
|
if (Platform.isTV) return null;
|
||||||
|
|
||||||
|
// Don't render if not visible
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.container,
|
||||||
|
animatedStyle,
|
||||||
|
{
|
||||||
|
top:
|
||||||
|
(settings?.safeAreaInControlsEnabled ?? true)
|
||||||
|
? insets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4
|
||||||
|
: HEADER_LAYOUT.CONTAINER_PADDING + 4,
|
||||||
|
left:
|
||||||
|
(settings?.safeAreaInControlsEnabled ?? true)
|
||||||
|
? insets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20
|
||||||
|
: HEADER_LAYOUT.CONTAINER_PADDING + 20,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
pointerEvents='none'
|
||||||
|
>
|
||||||
|
<View style={styles.infoBox}>
|
||||||
|
{playMethod && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.infoText,
|
||||||
|
{ color: getPlayMethodColor(playMethod) },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{getPlayMethodLabel(playMethod)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{transcodeReasons && transcodeReasons.length > 0 && (
|
||||||
|
<Text style={[styles.infoText, styles.reasonText]}>
|
||||||
|
{transcodeReasons.map(formatTranscodeReason).join(", ")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{info?.videoWidth && info?.videoHeight && (
|
||||||
|
<Text style={styles.infoText}>
|
||||||
|
{info.videoWidth}x{info.videoHeight}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{info?.videoCodec && (
|
||||||
|
<Text style={styles.infoText}>
|
||||||
|
Video: {formatCodec(info.videoCodec)}
|
||||||
|
{info.fps ? ` @ ${formatFps(info.fps)} fps` : ""}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{info?.audioCodec && (
|
||||||
|
<Text style={styles.infoText}>
|
||||||
|
Audio: {formatCodec(info.audioCodec)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{(info?.videoBitrate || info?.audioBitrate) && (
|
||||||
|
<Text style={styles.infoText}>
|
||||||
|
Bitrate:{" "}
|
||||||
|
{info.videoBitrate
|
||||||
|
? formatBitrate(info.videoBitrate)
|
||||||
|
: info.audioBitrate
|
||||||
|
? formatBitrate(info.audioBitrate)
|
||||||
|
: "N/A"}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{info?.cacheSeconds !== undefined && (
|
||||||
|
<Text style={styles.infoText}>
|
||||||
|
Buffer: {info.cacheSeconds.toFixed(1)}s
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{info?.droppedFrames !== undefined && info.droppedFrames > 0 && (
|
||||||
|
<Text style={[styles.infoText, styles.warningText]}>
|
||||||
|
Dropped: {info.droppedFrames} frames
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!info && !playMethod && (
|
||||||
|
<Text style={styles.infoText}>Loading...</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
TechnicalInfoOverlay.displayName = "TechnicalInfoOverlay";
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 15,
|
||||||
|
},
|
||||||
|
infoBox: {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
warningText: {
|
||||||
|
color: "#ff9800",
|
||||||
|
},
|
||||||
|
reasonText: {
|
||||||
|
color: "#fbbf24",
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -15,3 +15,7 @@ export const ICON_SIZES = {
|
|||||||
HEADER: 24,
|
HEADER: 24,
|
||||||
CENTER: 50,
|
CENTER: 50,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const HEADER_LAYOUT = {
|
||||||
|
CONTAINER_PADDING: 8, // p-2 = 8px (matches HeaderControls)
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ interface PlayerContextProps {
|
|||||||
mediaSource: MediaSourceInfo | null | undefined;
|
mediaSource: MediaSourceInfo | null | undefined;
|
||||||
isVideoLoaded: boolean;
|
isVideoLoaded: boolean;
|
||||||
tracksReady: boolean;
|
tracksReady: boolean;
|
||||||
offline: boolean;
|
|
||||||
downloadedItem: DownloadedItem | null;
|
downloadedItem: DownloadedItem | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ interface PlayerProviderProps {
|
|||||||
mediaSource: MediaSourceInfo | null | undefined;
|
mediaSource: MediaSourceInfo | null | undefined;
|
||||||
isVideoLoaded: boolean;
|
isVideoLoaded: boolean;
|
||||||
tracksReady: boolean;
|
tracksReady: boolean;
|
||||||
offline?: boolean;
|
|
||||||
downloadedItem?: DownloadedItem | null;
|
downloadedItem?: DownloadedItem | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ export const PlayerProvider: React.FC<PlayerProviderProps> = ({
|
|||||||
mediaSource,
|
mediaSource,
|
||||||
isVideoLoaded,
|
isVideoLoaded,
|
||||||
tracksReady,
|
tracksReady,
|
||||||
offline = false,
|
|
||||||
downloadedItem = null,
|
downloadedItem = null,
|
||||||
}) => {
|
}) => {
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
@@ -52,18 +49,9 @@ export const PlayerProvider: React.FC<PlayerProviderProps> = ({
|
|||||||
mediaSource,
|
mediaSource,
|
||||||
isVideoLoaded,
|
isVideoLoaded,
|
||||||
tracksReady,
|
tracksReady,
|
||||||
offline,
|
|
||||||
downloadedItem,
|
downloadedItem,
|
||||||
}),
|
}),
|
||||||
[
|
[playerRef, item, mediaSource, isVideoLoaded, tracksReady, downloadedItem],
|
||||||
playerRef,
|
|
||||||
item,
|
|
||||||
mediaSource,
|
|
||||||
isVideoLoaded,
|
|
||||||
tracksReady,
|
|
||||||
offline,
|
|
||||||
downloadedItem,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
|
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -57,7 +57,9 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import type { MpvAudioTrack } from "@/modules";
|
import type { MpvAudioTrack } from "@/modules";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
import { isImageBasedSubtitle } from "@/utils/jellyfin/subtitleUtils";
|
import { isImageBasedSubtitle } from "@/utils/jellyfin/subtitleUtils";
|
||||||
import type { Track } from "../types";
|
import type { Track } from "../types";
|
||||||
import { usePlayerContext, usePlayerControls } from "./PlayerContext";
|
import { usePlayerContext, usePlayerControls } from "./PlayerContext";
|
||||||
@@ -75,9 +77,10 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
||||||
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
||||||
|
|
||||||
const { tracksReady, mediaSource, offline, downloadedItem } =
|
const { tracksReady, mediaSource, downloadedItem } = usePlayerContext();
|
||||||
usePlayerContext();
|
|
||||||
const playerControls = usePlayerControls();
|
const playerControls = usePlayerControls();
|
||||||
|
const offline = useOfflineMode();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { itemId, audioIndex, bitrateValue, subtitleIndex, playbackPosition } =
|
const { itemId, audioIndex, bitrateValue, subtitleIndex, playbackPosition } =
|
||||||
useLocalSearchParams<{
|
useLocalSearchParams<{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
PlatformDropdown,
|
PlatformDropdown,
|
||||||
} from "@/components/PlatformDropdown";
|
} from "@/components/PlatformDropdown";
|
||||||
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
import { PLAYBACK_SPEEDS } from "@/components/PlaybackSpeedSelector";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { usePlayerContext } from "../contexts/PlayerContext";
|
import { usePlayerContext } from "../contexts/PlayerContext";
|
||||||
import { useVideoContext } from "../contexts/VideoContext";
|
import { useVideoContext } from "../contexts/VideoContext";
|
||||||
@@ -28,18 +30,23 @@ const SUBTITLE_SIZE_PRESETS = [
|
|||||||
interface DropdownViewProps {
|
interface DropdownViewProps {
|
||||||
playbackSpeed?: number;
|
playbackSpeed?: number;
|
||||||
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
setPlaybackSpeed?: (speed: number, scope: PlaybackSpeedScope) => void;
|
||||||
|
showTechnicalInfo?: boolean;
|
||||||
|
onToggleTechnicalInfo?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownView = ({
|
const DropdownView = ({
|
||||||
playbackSpeed = 1.0,
|
playbackSpeed = 1.0,
|
||||||
setPlaybackSpeed,
|
setPlaybackSpeed,
|
||||||
|
showTechnicalInfo = false,
|
||||||
|
onToggleTechnicalInfo,
|
||||||
}: DropdownViewProps) => {
|
}: DropdownViewProps) => {
|
||||||
const { subtitleTracks, audioTracks } = useVideoContext();
|
const { subtitleTracks, audioTracks } = useVideoContext();
|
||||||
const { item, mediaSource } = usePlayerContext();
|
const { item, mediaSource } = usePlayerContext();
|
||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
|
|
||||||
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition, offline } =
|
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
|
||||||
useLocalSearchParams<{
|
useLocalSearchParams<{
|
||||||
itemId: string;
|
itemId: string;
|
||||||
audioIndex: string;
|
audioIndex: string;
|
||||||
@@ -47,15 +54,12 @@ const DropdownView = ({
|
|||||||
mediaSourceId: string;
|
mediaSourceId: string;
|
||||||
bitrateValue: string;
|
bitrateValue: string;
|
||||||
playbackPosition: string;
|
playbackPosition: string;
|
||||||
offline: string;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Use ref to track playbackPosition without causing re-renders
|
// Use ref to track playbackPosition without causing re-renders
|
||||||
const playbackPositionRef = useRef(playbackPosition);
|
const playbackPositionRef = useRef(playbackPosition);
|
||||||
playbackPositionRef.current = playbackPosition;
|
playbackPositionRef.current = playbackPosition;
|
||||||
|
|
||||||
const isOffline = offline === "true";
|
|
||||||
|
|
||||||
// Stabilize IDs to prevent unnecessary recalculations
|
// Stabilize IDs to prevent unnecessary recalculations
|
||||||
const itemIdRef = useRef(item.Id);
|
const itemIdRef = useRef(item.Id);
|
||||||
const mediaSourceIdRef = useRef(mediaSource?.Id);
|
const mediaSourceIdRef = useRef(mediaSource?.Id);
|
||||||
@@ -161,6 +165,21 @@ const DropdownView = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Technical Info (at bottom)
|
||||||
|
if (onToggleTechnicalInfo) {
|
||||||
|
groups.push({
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: "action" as const,
|
||||||
|
label: showTechnicalInfo
|
||||||
|
? "Hide Technical Info"
|
||||||
|
: "Show Technical Info",
|
||||||
|
onPress: onToggleTechnicalInfo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
@@ -175,6 +194,8 @@ const DropdownView = ({
|
|||||||
updateSettings,
|
updateSettings,
|
||||||
playbackSpeed,
|
playbackSpeed,
|
||||||
setPlaybackSpeed,
|
setPlaybackSpeed,
|
||||||
|
showTechnicalInfo,
|
||||||
|
onToggleTechnicalInfo,
|
||||||
// Note: subtitleTracks and audioTracks are intentionally excluded
|
// Note: subtitleTracks and audioTracks are intentionally excluded
|
||||||
// because we use subtitleTracksKey and audioTracksKey for stability
|
// because we use subtitleTracksKey and audioTracksKey for stability
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
BottomSheetView,
|
BottomSheetView,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import {
|
import {
|
||||||
useAddToWatchlist,
|
useAddToWatchlist,
|
||||||
useRemoveFromWatchlist,
|
useRemoveFromWatchlist,
|
||||||
|
|||||||
86
hooks/useAppRouter.ts
Normal file
86
hooks/useAppRouter.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in replacement for expo-router's useRouter that automatically
|
||||||
|
* preserves offline state across navigation.
|
||||||
|
*
|
||||||
|
* - For object-form navigation, automatically adds offline=true when in offline context
|
||||||
|
* - For string URLs, passes through unchanged (caller handles offline param)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* import useRouter from "@/hooks/useAppRouter";
|
||||||
|
*
|
||||||
|
* const router = useRouter();
|
||||||
|
* router.push({ pathname: "/items/page", params: { id: item.Id } }); // offline added automatically
|
||||||
|
*/
|
||||||
|
export function useAppRouter() {
|
||||||
|
const router = useRouter();
|
||||||
|
const isOffline = useOfflineMode();
|
||||||
|
|
||||||
|
const push = useCallback(
|
||||||
|
(href: Parameters<typeof router.push>[0]) => {
|
||||||
|
if (typeof href === "string") {
|
||||||
|
router.push(href as any);
|
||||||
|
} else {
|
||||||
|
const callerParams = (href.params ?? {}) as Record<string, unknown>;
|
||||||
|
const hasExplicitOffline = "offline" in callerParams;
|
||||||
|
router.push({
|
||||||
|
...href,
|
||||||
|
params: {
|
||||||
|
// Only add offline if caller hasn't explicitly set it
|
||||||
|
...(isOffline && !hasExplicitOffline && { offline: "true" }),
|
||||||
|
...callerParams,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[router, isOffline],
|
||||||
|
);
|
||||||
|
|
||||||
|
const replace = useCallback(
|
||||||
|
(href: Parameters<typeof router.replace>[0]) => {
|
||||||
|
if (typeof href === "string") {
|
||||||
|
router.replace(href as any);
|
||||||
|
} else {
|
||||||
|
const callerParams = (href.params ?? {}) as Record<string, unknown>;
|
||||||
|
const hasExplicitOffline = "offline" in callerParams;
|
||||||
|
router.replace({
|
||||||
|
...href,
|
||||||
|
params: {
|
||||||
|
// Only add offline if caller hasn't explicitly set it
|
||||||
|
...(isOffline && !hasExplicitOffline && { offline: "true" }),
|
||||||
|
...callerParams,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[router, isOffline],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setParams = useCallback(
|
||||||
|
(params: Parameters<typeof router.setParams>[0]) => {
|
||||||
|
const callerParams = (params ?? {}) as Record<string, unknown>;
|
||||||
|
const hasExplicitOffline = "offline" in callerParams;
|
||||||
|
router.setParams({
|
||||||
|
// Only add offline if caller hasn't explicitly set it
|
||||||
|
...(isOffline && !hasExplicitOffline && { offline: "true" }),
|
||||||
|
...callerParams,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[router, isOffline],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
...router,
|
||||||
|
push,
|
||||||
|
replace,
|
||||||
|
setParams,
|
||||||
|
}),
|
||||||
|
[router, push, replace, setParams],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAppRouter;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,26 @@ export function useNetworkAwareQueryClient(): NetworkAwareQueryClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// Create a proxy-like object that inherits from queryClient
|
// Use a Proxy to wrap the queryClient and override invalidateQueries.
|
||||||
// but overrides invalidateQueries
|
// Object.create doesn't work because QueryClient uses private fields (#)
|
||||||
const wrapped = Object.create(queryClient) as NetworkAwareQueryClient;
|
// which can only be accessed on the exact instance they were defined on.
|
||||||
wrapped.invalidateQueries = networkAwareInvalidate;
|
const forceInvalidate = queryClient.invalidateQueries.bind(queryClient);
|
||||||
wrapped.forceInvalidateQueries =
|
|
||||||
queryClient.invalidateQueries.bind(queryClient);
|
return new Proxy(queryClient, {
|
||||||
return wrapped;
|
get(target, prop) {
|
||||||
|
if (prop === "invalidateQueries") {
|
||||||
|
return networkAwareInvalidate;
|
||||||
|
}
|
||||||
|
if (prop === "forceInvalidateQueries") {
|
||||||
|
return forceInvalidate;
|
||||||
|
}
|
||||||
|
const value = Reflect.get(target, prop, target);
|
||||||
|
// Bind methods to the original target to preserve private field access
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return value.bind(target);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
}) as NetworkAwareQueryClient;
|
||||||
}, [queryClient, networkAwareInvalidate]);
|
}, [queryClient, networkAwareInvalidate]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const orientationToOrientationLock = (
|
|||||||
return OrientationLock.LANDSCAPE_RIGHT;
|
return OrientationLock.LANDSCAPE_RIGHT;
|
||||||
case Orientation.PORTRAIT_UP:
|
case Orientation.PORTRAIT_UP:
|
||||||
return OrientationLock.PORTRAIT_UP;
|
return OrientationLock.PORTRAIT_UP;
|
||||||
|
case Orientation.UNKNOWN:
|
||||||
|
return OrientationLock.LANDSCAPE;
|
||||||
default:
|
default:
|
||||||
return OrientationLock.PORTRAIT_UP;
|
return OrientationLock.PORTRAIT_UP;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
PlaybackProgressInfo,
|
PlaybackProgressInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useDownload } from "@/providers/DownloadProvider";
|
import { useDownload } from "@/providers/DownloadProvider";
|
||||||
@@ -69,6 +69,7 @@ export const usePlaybackManager = ({
|
|||||||
const api = useAtomValue(apiAtom);
|
const api = useAtomValue(apiAtom);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
const { isConnected } = useNetworkStatus();
|
const { isConnected } = useNetworkStatus();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { getDownloadedItemById, updateDownloadedItem, getDownloadedItems } =
|
const { getDownloadedItemById, updateDownloadedItem, getDownloadedItems } =
|
||||||
useDownload();
|
useDownload();
|
||||||
|
|
||||||
@@ -186,6 +187,9 @@ export const usePlaybackManager = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Force invalidate queries so they refetch from updated local database
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["item", itemId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["episodes"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle remote state update if online
|
// Handle remote state update if online
|
||||||
@@ -226,6 +230,9 @@ export const usePlaybackManager = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Force invalidate queries so they refetch from updated local database
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["item", itemId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["episodes"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle remote state update if online
|
// Handle remote state update if online
|
||||||
@@ -268,6 +275,9 @@ export const usePlaybackManager = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Force invalidate queries so they refetch from updated local database
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["item", itemId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["episodes"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle remote state update if online
|
// Handle remote state update if online
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
import { useWebSocketContext } from "@/providers/WebSocketProvider";
|
||||||
|
|
||||||
interface UseWebSocketProps {
|
interface UseWebSocketProps {
|
||||||
|
|||||||
97
hooks/useWifiSSID.ts
Normal file
97
hooks/useWifiSSID.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import * as Location from "expo-location";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getSSID } from "@/modules/wifi-ssid";
|
||||||
|
|
||||||
|
export type PermissionStatus =
|
||||||
|
| "granted"
|
||||||
|
| "denied"
|
||||||
|
| "undetermined"
|
||||||
|
| "unavailable";
|
||||||
|
|
||||||
|
export interface UseWifiSSIDReturn {
|
||||||
|
ssid: string | null;
|
||||||
|
permissionStatus: PermissionStatus;
|
||||||
|
requestPermission: () => Promise<boolean>;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapLocationStatus(
|
||||||
|
status: Location.PermissionStatus,
|
||||||
|
): PermissionStatus {
|
||||||
|
switch (status) {
|
||||||
|
case Location.PermissionStatus.GRANTED:
|
||||||
|
return "granted";
|
||||||
|
case Location.PermissionStatus.DENIED:
|
||||||
|
return "denied";
|
||||||
|
default:
|
||||||
|
return "undetermined";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWifiSSID(): UseWifiSSIDReturn {
|
||||||
|
const [ssid, setSSID] = useState<string | null>(null);
|
||||||
|
const [permissionStatus, setPermissionStatus] =
|
||||||
|
useState<PermissionStatus>("undetermined");
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchSSID = useCallback(async () => {
|
||||||
|
const result = await getSSID();
|
||||||
|
console.log("[WiFi Debug] Native module SSID:", result);
|
||||||
|
setSSID(result);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestPermission = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||||
|
const newStatus = mapLocationStatus(status);
|
||||||
|
setPermissionStatus(newStatus);
|
||||||
|
|
||||||
|
if (newStatus === "granted") {
|
||||||
|
await fetchSSID();
|
||||||
|
}
|
||||||
|
|
||||||
|
return newStatus === "granted";
|
||||||
|
} catch {
|
||||||
|
setPermissionStatus("unavailable");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [fetchSSID]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function initialize() {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const { status } = await Location.getForegroundPermissionsAsync();
|
||||||
|
const mappedStatus = mapLocationStatus(status);
|
||||||
|
setPermissionStatus(mappedStatus);
|
||||||
|
|
||||||
|
if (mappedStatus === "granted") {
|
||||||
|
await fetchSSID();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setPermissionStatus("unavailable");
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
}, [fetchSSID]);
|
||||||
|
|
||||||
|
// Refresh SSID when permission status changes to granted
|
||||||
|
useEffect(() => {
|
||||||
|
if (permissionStatus === "granted") {
|
||||||
|
fetchSSID();
|
||||||
|
|
||||||
|
// Also set up an interval to periodically check SSID
|
||||||
|
const interval = setInterval(fetchSSID, 10000); // Check every 10 seconds
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [permissionStatus, fetchSSID]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ssid,
|
||||||
|
permissionStatus,
|
||||||
|
requestPermission,
|
||||||
|
isLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -50,6 +50,23 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
|||||||
private var _isLoading: Boolean = false
|
private var _isLoading: Boolean = false
|
||||||
private var _playbackSpeed: Double = 1.0
|
private var _playbackSpeed: Double = 1.0
|
||||||
private var isReadyToSeek: Boolean = false
|
private var isReadyToSeek: Boolean = false
|
||||||
|
|
||||||
|
// Progress update throttling - CRITICAL for performance!
|
||||||
|
// DO NOT REMOVE THIS THROTTLE - it is essential for battery life and CPU efficiency.
|
||||||
|
//
|
||||||
|
// Without throttling, time-pos fires every video frame (24+ times/sec at 24fps).
|
||||||
|
// Each update crosses the React Native JS bridge, which is expensive on mobile.
|
||||||
|
// Even if the JS side does nothing, 24+ bridge calls/sec wastes CPU and battery.
|
||||||
|
//
|
||||||
|
// Throttling to 1 update/sec during normal playback is sufficient for:
|
||||||
|
// - Progress bar updates (users can't perceive 1-second granularity)
|
||||||
|
// - Playback position tracking
|
||||||
|
// - Any JS-side logic that needs current position
|
||||||
|
//
|
||||||
|
// During seeking, we bypass the throttle for responsive scrubbing.
|
||||||
|
// This optimization reduced CPU usage by ~50% for downloaded file playback.
|
||||||
|
private var lastProgressUpdateTime: Long = 0
|
||||||
|
private var _isSeeking: Boolean = false
|
||||||
|
|
||||||
// Video dimensions
|
// Video dimensions
|
||||||
private var _videoWidth: Int = 0
|
private var _videoWidth: Int = 0
|
||||||
@@ -430,6 +447,57 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
|||||||
MPVLib.setPropertyDouble("panscan", panscanValue)
|
MPVLib.setPropertyDouble("panscan", panscanValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Technical Info
|
||||||
|
|
||||||
|
fun getTechnicalInfo(): Map<String, Any> {
|
||||||
|
val info = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
// Video dimensions
|
||||||
|
MPVLib.getPropertyInt("video-params/w")?.takeIf { it > 0 }?.let {
|
||||||
|
info["videoWidth"] = it
|
||||||
|
}
|
||||||
|
MPVLib.getPropertyInt("video-params/h")?.takeIf { it > 0 }?.let {
|
||||||
|
info["videoHeight"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video codec
|
||||||
|
MPVLib.getPropertyString("video-format")?.let {
|
||||||
|
info["videoCodec"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio codec
|
||||||
|
MPVLib.getPropertyString("audio-codec-name")?.let {
|
||||||
|
info["audioCodec"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPS (container fps)
|
||||||
|
MPVLib.getPropertyDouble("container-fps")?.takeIf { it > 0 }?.let {
|
||||||
|
info["fps"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video bitrate (bits per second)
|
||||||
|
MPVLib.getPropertyInt("video-bitrate")?.takeIf { it > 0 }?.let {
|
||||||
|
info["videoBitrate"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio bitrate (bits per second)
|
||||||
|
MPVLib.getPropertyInt("audio-bitrate")?.takeIf { it > 0 }?.let {
|
||||||
|
info["audioBitrate"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demuxer cache duration (seconds of video buffered)
|
||||||
|
MPVLib.getPropertyDouble("demuxer-cache-duration")?.let {
|
||||||
|
info["cacheSeconds"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropped frames
|
||||||
|
MPVLib.getPropertyInt("frame-drop-count")?.let {
|
||||||
|
info["droppedFrames"] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - MPVLib.EventObserver
|
// MARK: - MPVLib.EventObserver
|
||||||
|
|
||||||
override fun eventProperty(property: String) {
|
override fun eventProperty(property: String) {
|
||||||
@@ -497,7 +565,13 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
|||||||
}
|
}
|
||||||
"time-pos" -> {
|
"time-pos" -> {
|
||||||
cachedPosition = value
|
cachedPosition = value
|
||||||
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) }
|
// Always update immediately when seeking, otherwise throttle to once per second
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val shouldUpdate = _isSeeking || (now - lastProgressUpdateTime >= 1000)
|
||||||
|
if (shouldUpdate) {
|
||||||
|
lastProgressUpdateTime = now
|
||||||
|
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,8 +581,10 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
|||||||
MPVLib.MPV_EVENT_FILE_LOADED -> {
|
MPVLib.MPV_EVENT_FILE_LOADED -> {
|
||||||
// Add external subtitles now that file is loaded
|
// Add external subtitles now that file is loaded
|
||||||
if (pendingExternalSubtitles.isNotEmpty()) {
|
if (pendingExternalSubtitles.isNotEmpty()) {
|
||||||
for (subUrl in pendingExternalSubtitles) {
|
pendingExternalSubtitles.forEachIndexed { index, subUrl ->
|
||||||
MPVLib.command(arrayOf("sub-add", subUrl))
|
android.util.Log.d("MPVRenderer", "Adding external subtitle [$index]: $subUrl")
|
||||||
|
// "auto" flag = add without auto-selecting (order preserved, MPVLib.command is sync)
|
||||||
|
MPVLib.command(arrayOf("sub-add", subUrl, "auto"))
|
||||||
}
|
}
|
||||||
pendingExternalSubtitles = emptyList()
|
pendingExternalSubtitles = emptyList()
|
||||||
|
|
||||||
@@ -527,7 +603,8 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MPVLib.MPV_EVENT_SEEK -> {
|
MPVLib.MPV_EVENT_SEEK -> {
|
||||||
// Seek started - show loading indicator
|
// Seek started - show loading indicator and enable immediate progress updates
|
||||||
|
_isSeeking = true
|
||||||
if (!_isLoading) {
|
if (!_isLoading) {
|
||||||
_isLoading = true
|
_isLoading = true
|
||||||
mainHandler.post { delegate?.onLoadingChanged(true) }
|
mainHandler.post { delegate?.onLoadingChanged(true) }
|
||||||
@@ -535,6 +612,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
|||||||
}
|
}
|
||||||
MPVLib.MPV_EVENT_PLAYBACK_RESTART -> {
|
MPVLib.MPV_EVENT_PLAYBACK_RESTART -> {
|
||||||
// Video playback has started/restarted (including after seek)
|
// Video playback has started/restarted (including after seek)
|
||||||
|
_isSeeking = false
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
_isLoading = false
|
_isLoading = false
|
||||||
mainHandler.post { delegate?.onLoadingChanged(false) }
|
mainHandler.post { delegate?.onLoadingChanged(false) }
|
||||||
|
|||||||
@@ -173,6 +173,11 @@ class MpvPlayerModule : Module() {
|
|||||||
view.isZoomedToFill()
|
view.isZoomedToFill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Technical info function
|
||||||
|
AsyncFunction("getTechnicalInfo") { view: MpvPlayerView ->
|
||||||
|
view.getTechnicalInfo()
|
||||||
|
}
|
||||||
|
|
||||||
// Defines events that the view can send to JavaScript
|
// Defines events that the view can send to JavaScript
|
||||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
|
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,23 +33,6 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MpvPlayerView"
|
private const val TAG = "MpvPlayerView"
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if running on an Android emulator.
|
|
||||||
* MPV player has EGL/OpenGL compatibility issues on emulators.
|
|
||||||
*/
|
|
||||||
private fun isEmulator(): Boolean {
|
|
||||||
return (Build.FINGERPRINT.startsWith("generic")
|
|
||||||
|| Build.FINGERPRINT.startsWith("unknown")
|
|
||||||
|| Build.MODEL.contains("google_sdk")
|
|
||||||
|| Build.MODEL.contains("Emulator")
|
|
||||||
|| Build.MODEL.contains("Android SDK built for x86")
|
|
||||||
|| Build.MANUFACTURER.contains("Genymotion")
|
|
||||||
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|
|
||||||
|| "google_sdk" == Build.PRODUCT
|
|
||||||
|| Build.HARDWARE.contains("goldfish")
|
|
||||||
|| Build.HARDWARE.contains("ranchu"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event dispatchers
|
// Event dispatchers
|
||||||
@@ -104,21 +87,14 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the renderer (skip on emulators to avoid EGL crashes)
|
// Start the renderer
|
||||||
if (isEmulator()) {
|
try {
|
||||||
Log.w(TAG, "Running on emulator - MPV player disabled due to EGL/OpenGL compatibility issues")
|
renderer?.start()
|
||||||
// Don't start renderer on emulator, will show error when trying to play
|
} catch (e: Exception) {
|
||||||
} else {
|
Log.e(TAG, "Failed to start renderer: ${e.message}")
|
||||||
try {
|
onError(mapOf("error" to "Failed to start renderer: ${e.message}"))
|
||||||
renderer?.start()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to start renderer: ${e.message}")
|
|
||||||
onError(mapOf("error" to "Failed to start renderer: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isOnEmulator: Boolean = isEmulator()
|
|
||||||
|
|
||||||
// MARK: - SurfaceHolder.Callback
|
// MARK: - SurfaceHolder.Callback
|
||||||
|
|
||||||
@@ -149,13 +125,6 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
// MARK: - Video Loading
|
// MARK: - Video Loading
|
||||||
|
|
||||||
fun loadVideo(config: VideoLoadConfig) {
|
fun loadVideo(config: VideoLoadConfig) {
|
||||||
// Block video loading on emulators
|
|
||||||
if (isOnEmulator) {
|
|
||||||
Log.w(TAG, "Cannot load video on emulator - MPV player not supported")
|
|
||||||
onError(mapOf("error" to "MPV player is not supported on emulators. Please test on a real device."))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip reload if same URL is already playing
|
// Skip reload if same URL is already playing
|
||||||
if (currentUrl == config.url) {
|
if (currentUrl == config.url) {
|
||||||
return
|
return
|
||||||
@@ -330,6 +299,12 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
return _isZoomedToFill
|
return _isZoomedToFill
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Technical Info
|
||||||
|
|
||||||
|
fun getTechnicalInfo(): Map<String, Any> {
|
||||||
|
return renderer?.getTechnicalInfo() ?: emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - MPVLayerRenderer.Delegate
|
// MARK: - MPVLayerRenderer.Delegate
|
||||||
|
|
||||||
override fun onPositionChanged(position: Double, duration: Double) {
|
override fun onPositionChanged(position: Double, duration: Double) {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ final class MPVLayerRenderer {
|
|||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
private var isStopping = false
|
private var isStopping = false
|
||||||
|
|
||||||
|
// KVO observation for display layer status
|
||||||
|
private var statusObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
weak var delegate: MPVLayerRendererDelegate?
|
weak var delegate: MPVLayerRendererDelegate?
|
||||||
|
|
||||||
// Thread-safe state for playback
|
// Thread-safe state for playback
|
||||||
@@ -45,6 +48,23 @@ final class MPVLayerRenderer {
|
|||||||
private var _playbackSpeed: Double = 1.0
|
private var _playbackSpeed: Double = 1.0
|
||||||
private var _isLoading: Bool = false
|
private var _isLoading: Bool = false
|
||||||
private var _isReadyToSeek: Bool = false
|
private var _isReadyToSeek: Bool = false
|
||||||
|
private var _isSeeking: Bool = false
|
||||||
|
|
||||||
|
// Progress update throttling - CRITICAL for performance!
|
||||||
|
// DO NOT REMOVE THIS THROTTLE - it is essential for battery life and CPU efficiency.
|
||||||
|
//
|
||||||
|
// Without throttling, time-pos fires every video frame (24+ times/sec at 24fps).
|
||||||
|
// Each update crosses the React Native JS bridge, which is expensive on mobile.
|
||||||
|
// Even if the JS side does nothing, 24+ bridge calls/sec wastes CPU and battery.
|
||||||
|
//
|
||||||
|
// Throttling to 1 update/sec during normal playback is sufficient for:
|
||||||
|
// - Progress bar updates (users can't perceive 1-second granularity)
|
||||||
|
// - Playback position tracking
|
||||||
|
// - Any JS-side logic that needs current position
|
||||||
|
//
|
||||||
|
// During seeking, we bypass the throttle for responsive scrubbing.
|
||||||
|
// This optimization reduced CPU usage by ~50% for downloaded file playback.
|
||||||
|
private var lastProgressUpdateTime: CFAbsoluteTime = 0
|
||||||
|
|
||||||
// Thread-safe accessors
|
// Thread-safe accessors
|
||||||
private var cachedDuration: Double {
|
private var cachedDuration: Double {
|
||||||
@@ -71,6 +91,10 @@ final class MPVLayerRenderer {
|
|||||||
get { stateQueue.sync { _isReadyToSeek } }
|
get { stateQueue.sync { _isReadyToSeek } }
|
||||||
set { stateQueue.async(flags: .barrier) { self._isReadyToSeek = newValue } }
|
set { stateQueue.async(flags: .barrier) { self._isReadyToSeek = newValue } }
|
||||||
}
|
}
|
||||||
|
private var isSeeking: Bool {
|
||||||
|
get { stateQueue.sync { _isSeeking } }
|
||||||
|
set { stateQueue.async(flags: .barrier) { self._isSeeking = newValue } }
|
||||||
|
}
|
||||||
|
|
||||||
var isPausedState: Bool {
|
var isPausedState: Bool {
|
||||||
return isPaused
|
return isPaused
|
||||||
@@ -78,6 +102,37 @@ final class MPVLayerRenderer {
|
|||||||
|
|
||||||
init(displayLayer: AVSampleBufferDisplayLayer) {
|
init(displayLayer: AVSampleBufferDisplayLayer) {
|
||||||
self.displayLayer = displayLayer
|
self.displayLayer = displayLayer
|
||||||
|
observeDisplayLayerStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Watches for display layer failures and auto-recovers.
|
||||||
|
///
|
||||||
|
/// iOS aggressively kills VideoToolbox decoder sessions when the app is
|
||||||
|
/// backgrounded, the screen is locked, or system resources are low.
|
||||||
|
/// This causes the video to go black - especially problematic for PiP.
|
||||||
|
///
|
||||||
|
/// This KVO observer detects when the display layer status becomes `.failed`
|
||||||
|
/// and automatically reinitializes the hardware decoder to restore video.
|
||||||
|
private func observeDisplayLayerStatus() {
|
||||||
|
statusObservation = displayLayer.observe(\.status, options: [.new]) { [weak self] layer, _ in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
if layer.status == .failed {
|
||||||
|
print("🔧 Display layer failed - auto-resetting decoder")
|
||||||
|
self.queue.async {
|
||||||
|
self.performDecoderReset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actually performs the decoder reset (called by observer or manually)
|
||||||
|
private func performDecoderReset() {
|
||||||
|
guard let handle = mpv else { return }
|
||||||
|
print("🔧 Resetting decoder: status=\(displayLayer.status.rawValue), requiresFlush=\(displayLayer.requiresFlushToResumeDecoding)")
|
||||||
|
commandSync(handle, ["set", "hwdec", "no"])
|
||||||
|
commandSync(handle, ["set", "hwdec", "auto"])
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -150,6 +205,10 @@ final class MPVLayerRenderer {
|
|||||||
isRunning = false
|
isRunning = false
|
||||||
isStopping = true
|
isStopping = true
|
||||||
|
|
||||||
|
// Stop observing display layer status
|
||||||
|
statusObservation?.invalidate()
|
||||||
|
statusObservation = nil
|
||||||
|
|
||||||
queue.sync { [weak self] in
|
queue.sync { [weak self] in
|
||||||
guard let self, let handle = self.mpv else { return }
|
guard let self, let handle = self.mpv else { return }
|
||||||
|
|
||||||
@@ -339,8 +398,11 @@ final class MPVLayerRenderer {
|
|||||||
// Add external subtitles now that the file is loaded
|
// Add external subtitles now that the file is loaded
|
||||||
let hadExternalSubs = !pendingExternalSubtitles.isEmpty
|
let hadExternalSubs = !pendingExternalSubtitles.isEmpty
|
||||||
if hadExternalSubs, let handle = mpv {
|
if hadExternalSubs, let handle = mpv {
|
||||||
for subUrl in pendingExternalSubtitles {
|
for (index, subUrl) in pendingExternalSubtitles.enumerated() {
|
||||||
command(handle, ["sub-add", subUrl])
|
print("🔧 Adding external subtitle [\(index)]: \(subUrl)")
|
||||||
|
// Use commandSync to ensure subs are added in exact order (not async)
|
||||||
|
// "auto" flag = add without auto-selecting
|
||||||
|
commandSync(handle, ["sub-add", subUrl, "auto"])
|
||||||
}
|
}
|
||||||
pendingExternalSubtitles = []
|
pendingExternalSubtitles = []
|
||||||
// Set subtitle after external subs are added
|
// Set subtitle after external subs are added
|
||||||
@@ -367,7 +429,8 @@ final class MPVLayerRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case MPV_EVENT_SEEK:
|
case MPV_EVENT_SEEK:
|
||||||
// Seek started - show loading indicator
|
// Seek started - show loading indicator and enable immediate progress updates
|
||||||
|
isSeeking = true
|
||||||
if !isLoading {
|
if !isLoading {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -378,6 +441,7 @@ final class MPVLayerRenderer {
|
|||||||
|
|
||||||
case MPV_EVENT_PLAYBACK_RESTART:
|
case MPV_EVENT_PLAYBACK_RESTART:
|
||||||
// Video playback has started/restarted (including after seek)
|
// Video playback has started/restarted (including after seek)
|
||||||
|
isSeeking = false
|
||||||
if isLoading {
|
if isLoading {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -428,9 +492,15 @@ final class MPVLayerRenderer {
|
|||||||
let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value)
|
let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value)
|
||||||
if status >= 0 {
|
if status >= 0 {
|
||||||
cachedPosition = value
|
cachedPosition = value
|
||||||
DispatchQueue.main.async { [weak self] in
|
// Always update immediately when seeking, otherwise throttle to once per second
|
||||||
guard let self else { return }
|
let now = CFAbsoluteTimeGetCurrent()
|
||||||
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration)
|
let shouldUpdate = isSeeking || (now - lastProgressUpdateTime >= 1.0)
|
||||||
|
if shouldUpdate {
|
||||||
|
lastProgressUpdateTime = now
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "pause":
|
case "pause":
|
||||||
@@ -531,7 +601,9 @@ final class MPVLayerRenderer {
|
|||||||
cachedPosition = clamped
|
cachedPosition = clamped
|
||||||
commandSync(handle, ["seek", String(clamped), "absolute"])
|
commandSync(handle, ["seek", String(clamped), "absolute"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func seek(by seconds: Double) {
|
func seek(by seconds: Double) {
|
||||||
guard let handle = mpv else { return }
|
guard let handle = mpv else { return }
|
||||||
let newPosition = max(0, cachedPosition + seconds)
|
let newPosition = max(0, cachedPosition + seconds)
|
||||||
@@ -720,4 +792,64 @@ final class MPVLayerRenderer {
|
|||||||
getProperty(handle: handle, name: "aid", format: MPV_FORMAT_INT64, value: &aid)
|
getProperty(handle: handle, name: "aid", format: MPV_FORMAT_INT64, value: &aid)
|
||||||
return Int(aid)
|
return Int(aid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Technical Info
|
||||||
|
|
||||||
|
func getTechnicalInfo() -> [String: Any] {
|
||||||
|
guard let handle = mpv else { return [:] }
|
||||||
|
|
||||||
|
var info: [String: Any] = [:]
|
||||||
|
|
||||||
|
// Video dimensions
|
||||||
|
var videoWidth: Int64 = 0
|
||||||
|
var videoHeight: Int64 = 0
|
||||||
|
if getProperty(handle: handle, name: "video-params/w", format: MPV_FORMAT_INT64, value: &videoWidth) >= 0 {
|
||||||
|
info["videoWidth"] = Int(videoWidth)
|
||||||
|
}
|
||||||
|
if getProperty(handle: handle, name: "video-params/h", format: MPV_FORMAT_INT64, value: &videoHeight) >= 0 {
|
||||||
|
info["videoHeight"] = Int(videoHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video codec
|
||||||
|
if let videoCodec = getStringProperty(handle: handle, name: "video-format") {
|
||||||
|
info["videoCodec"] = videoCodec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio codec
|
||||||
|
if let audioCodec = getStringProperty(handle: handle, name: "audio-codec-name") {
|
||||||
|
info["audioCodec"] = audioCodec
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPS (container fps)
|
||||||
|
var fps: Double = 0
|
||||||
|
if getProperty(handle: handle, name: "container-fps", format: MPV_FORMAT_DOUBLE, value: &fps) >= 0 && fps > 0 {
|
||||||
|
info["fps"] = fps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video bitrate (bits per second)
|
||||||
|
var videoBitrate: Int64 = 0
|
||||||
|
if getProperty(handle: handle, name: "video-bitrate", format: MPV_FORMAT_INT64, value: &videoBitrate) >= 0 && videoBitrate > 0 {
|
||||||
|
info["videoBitrate"] = Int(videoBitrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio bitrate (bits per second)
|
||||||
|
var audioBitrate: Int64 = 0
|
||||||
|
if getProperty(handle: handle, name: "audio-bitrate", format: MPV_FORMAT_INT64, value: &audioBitrate) >= 0 && audioBitrate > 0 {
|
||||||
|
info["audioBitrate"] = Int(audioBitrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demuxer cache duration (seconds of video buffered)
|
||||||
|
var cacheSeconds: Double = 0
|
||||||
|
if getProperty(handle: handle, name: "demuxer-cache-duration", format: MPV_FORMAT_DOUBLE, value: &cacheSeconds) >= 0 {
|
||||||
|
info["cacheSeconds"] = cacheSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropped frames
|
||||||
|
var droppedFrames: Int64 = 0
|
||||||
|
if getProperty(handle: handle, name: "frame-drop-count", format: MPV_FORMAT_INT64, value: &droppedFrames) >= 0 {
|
||||||
|
info["droppedFrames"] = Int(droppedFrames)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,11 @@ public class MpvPlayerModule: Module {
|
|||||||
return view.isZoomedToFill()
|
return view.isZoomedToFill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Technical info function
|
||||||
|
AsyncFunction("getTechnicalInfo") { (view: MpvPlayerView) -> [String: Any] in
|
||||||
|
return view.getTechnicalInfo()
|
||||||
|
}
|
||||||
|
|
||||||
// Defines events that the view can send to JavaScript
|
// Defines events that the view can send to JavaScript
|
||||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
|
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,13 @@ class MpvPlayerView: ExpoView {
|
|||||||
private var currentURL: URL?
|
private var currentURL: URL?
|
||||||
private var cachedPosition: Double = 0
|
private var cachedPosition: Double = 0
|
||||||
private var cachedDuration: Double = 0
|
private var cachedDuration: Double = 0
|
||||||
private var intendedPlayState: Bool = false // For PiP - ignores transient states during seek
|
private var intendedPlayState: Bool = false
|
||||||
private var _isZoomedToFill: Bool = false
|
private var _isZoomedToFill: Bool = false
|
||||||
|
|
||||||
required init(appContext: AppContext? = nil) {
|
required init(appContext: AppContext? = nil) {
|
||||||
super.init(appContext: appContext)
|
super.init(appContext: appContext)
|
||||||
setupView()
|
setupView()
|
||||||
|
// Note: Decoder reset is handled automatically via KVO in MPVLayerRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupView() {
|
private func setupView() {
|
||||||
@@ -281,6 +282,12 @@ class MpvPlayerView: ExpoView {
|
|||||||
return _isZoomedToFill
|
return _isZoomedToFill
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Technical Info
|
||||||
|
|
||||||
|
func getTechnicalInfo() -> [String: Any] {
|
||||||
|
return renderer?.getTechnicalInfo() ?? [:]
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
pipController?.stopPictureInPicture()
|
pipController?.stopPictureInPicture()
|
||||||
renderer?.stop()
|
renderer?.stop()
|
||||||
@@ -361,6 +368,11 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
renderer?.syncTimebase()
|
renderer?.syncTimebase()
|
||||||
// Set current time for PiP progress bar
|
// Set current time for PiP progress bar
|
||||||
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
|
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
|
||||||
|
|
||||||
|
// Reset to fit for PiP (zoomed video doesn't display correctly in PiP)
|
||||||
|
if _isZoomedToFill {
|
||||||
|
displayLayer.videoGravity = .resizeAspect
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pipController(_ controller: PiPController, didStartPictureInPicture: Bool) {
|
func pipController(_ controller: PiPController, didStartPictureInPicture: Bool) {
|
||||||
@@ -380,6 +392,11 @@ extension MpvPlayerView: PiPControllerDelegate {
|
|||||||
// Ensure timebase is synced after PiP ends
|
// Ensure timebase is synced after PiP ends
|
||||||
renderer?.syncTimebase()
|
renderer?.syncTimebase()
|
||||||
pipController?.updatePlaybackState()
|
pipController?.updatePlaybackState()
|
||||||
|
|
||||||
|
// Restore the user's zoom preference
|
||||||
|
if _isZoomedToFill {
|
||||||
|
displayLayer.videoGravity = .resizeAspectFill
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) {
|
func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import UIKit
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
final class SampleBufferDisplayView: UIView {
|
|
||||||
override class var layerClass: AnyClass { AVSampleBufferDisplayLayer.self }
|
|
||||||
|
|
||||||
var displayLayer: AVSampleBufferDisplayLayer {
|
|
||||||
return layer as! AVSampleBufferDisplayLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
private(set) var pipController: PiPController?
|
|
||||||
|
|
||||||
weak var pipDelegate: PiPControllerDelegate? {
|
|
||||||
didSet {
|
|
||||||
pipController?.delegate = pipDelegate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
commonInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
commonInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func commonInit() {
|
|
||||||
backgroundColor = .black
|
|
||||||
displayLayer.videoGravity = .resizeAspect
|
|
||||||
#if !os(tvOS)
|
|
||||||
#if compiler(>=6.0)
|
|
||||||
if #available(iOS 26.0, *) {
|
|
||||||
displayLayer.preferredDynamicRange = .automatic
|
|
||||||
} else if #available(iOS 17.0, *) {
|
|
||||||
displayLayer.wantsExtendedDynamicRangeContent = true
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if #available(iOS 17.0, *) {
|
|
||||||
displayLayer.wantsExtendedDynamicRangeContent = true
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
setupPictureInPicture()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupPictureInPicture() {
|
|
||||||
pipController = PiPController(sampleBufferDisplayLayer: displayLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - PiP Control Methods
|
|
||||||
|
|
||||||
func startPictureInPicture() {
|
|
||||||
pipController?.startPictureInPicture()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopPictureInPicture() {
|
|
||||||
pipController?.stopPictureInPicture()
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPictureInPictureSupported: Bool {
|
|
||||||
return pipController?.isPictureInPictureSupported ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPictureInPictureActive: Bool {
|
|
||||||
return pipController?.isPictureInPictureActive ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPictureInPicturePossible: Bool {
|
|
||||||
return pipController?.isPictureInPicturePossible ?? false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user