diff --git a/.claude/agents/tv-validator.md b/.claude/agents/tv-validator.md
new file mode 100644
index 000000000..a38dd751a
--- /dev/null
+++ b/.claude/agents/tv-validator.md
@@ -0,0 +1,103 @@
+---
+name: tv-validator
+description: Use this agent to review TV platform code for correct patterns and conventions. Use proactively after writing or modifying TV components. Validates focus handling, modal patterns, typography, list components, and other TV-specific requirements.
+tools: Read, Glob, Grep
+model: haiku
+color: blue
+---
+
+You are a TV platform code reviewer for Streamyfin, a React Native app with Apple TV and Android TV support. Review code for correct TV patterns and flag violations.
+
+## Critical Rules to Check
+
+### 1. No .tv.tsx File Suffix
+The `.tv.tsx` suffix does NOT work in this project. Metro bundler doesn't resolve it.
+
+**Violation**: Creating files like `MyComponent.tv.tsx` expecting auto-resolution
+**Correct**: Use `Platform.isTV` conditional rendering in the main file:
+```typescript
+if (Platform.isTV) {
+ return ;
+}
+return ;
+```
+
+### 2. No FlashList on TV
+FlashList has focus issues on TV. Use FlatList instead.
+
+**Violation**: `
+) : (
+
+)}
+```
+
+### 3. Modal Pattern
+Never use overlay/absolute-positioned modals on TV. They break back button handling.
+
+**Violation**: `position: "absolute"` or `Modal` component for TV overlays
+**Correct**: Use navigation-based pattern:
+- Create Jotai atom for state
+- Hook that sets atom and calls `router.push()`
+- Page in `app/(auth)/` that reads atom
+- `Stack.Screen` with `presentation: "transparentModal"`
+
+### 4. Typography
+All TV text must use `TVTypography` component.
+
+**Violation**: Raw `` in TV components
+**Correct**: `...`
+
+### 5. No Purple Accent Colors
+TV uses white for focus states, not purple.
+
+**Violation**: Purple/violet colors in TV focused states
+**Correct**: White (`#fff`, `white`) for focused states with `expo-blur` for backgrounds
+
+### 6. Focus Handling
+- Only ONE element should have `hasTVPreferredFocus={true}`
+- Focusable items need `disabled={isModalOpen}` when overlays are visible
+- Use `onFocus`/`onBlur` with scale animations
+- Add padding for scale animations (focus scale clips without it)
+
+### 7. List Configuration
+TV lists need:
+- `removeClippedSubviews={false}`
+- `overflow: "visible"` on containers
+- Sufficient padding for focus scale animations
+
+### 8. Horizontal Padding
+Use `TV_HORIZONTAL_PADDING` constant (60), not old `TV_SCALE_PADDING` (20).
+
+### 9. Focus Guide Navigation
+For non-adjacent sections, use `TVFocusGuideView` with `destinations` prop.
+Use `useState` for refs (not `useRef`) to trigger re-renders.
+
+## Review Process
+
+1. Read the file(s) to review
+2. Check each rule above
+3. Report violations with:
+ - Line number
+ - What's wrong
+ - How to fix it
+4. If no violations, confirm the code follows TV patterns
+
+## Output Format
+
+```
+## TV Validation Results
+
+### ✓ Passes
+- [List of rules that pass]
+
+### ✗ Violations
+- **[Rule Name]** (line X): [Description]
+ Fix: [How to correct it]
+
+### Recommendations
+- [Optional suggestions for improvement]
+```
diff --git a/.claude/commands/reflect.md b/.claude/commands/reflect.md
index 2ee234799..deedf8d4f 100644
--- a/.claude/commands/reflect.md
+++ b/.claude/commands/reflect.md
@@ -12,26 +12,59 @@ Analyze the current conversation to extract useful facts that should be remember
## Instructions
-1. Read the existing facts file at `.claude/learned-facts.md`
+1. Read the Learned Facts Index section in `CLAUDE.md` and scan existing files in `.claude/learned-facts/` to understand what's already recorded
2. Review this conversation for learnings worth preserving
3. For each new fact:
- - Write it concisely (1-2 sentences max)
- - Include context for why it matters
- - Add today's date
+ - Create a new file in `.claude/learned-facts/[kebab-case-name].md` using the template below
+ - Append a new entry to the appropriate category in the **Learned Facts Index** section of `CLAUDE.md`
4. Skip facts that duplicate existing entries
-5. Append new facts to `.claude/learned-facts.md`
+5. If a new category is needed, add it to the index in `CLAUDE.md`
-## Fact Format
+## Fact File Template
-Use this format for each fact:
-```
-- **[Brief Topic]**: [Concise description of the fact] _(YYYY-MM-DD)_
+Create each file at `.claude/learned-facts/[kebab-case-name].md`:
+
+```markdown
+# [Title]
+
+**Date**: YYYY-MM-DD
+**Category**: navigation | tv | native-modules | state-management | ui
+**Key files**: `relevant/paths.ts`
+
+## Detail
+
+[Full description of the fact, including context for why it matters]
```
-## Example Facts
+## Index Entry Format
-- **State management**: Use Jotai atoms for global state, NOT React Context - atoms are in `utils/atoms/` _(2025-01-09)_
-- **Package manager**: Always use `bun`, never npm or yarn - the project is configured for bun only _(2025-01-09)_
-- **TV platform**: Check `Platform.isTV` for TV-specific code paths, not just OS checks _(2025-01-09)_
+Append to the appropriate category in the Learned Facts Index section of `CLAUDE.md`:
-After updating the file, summarize what facts you added (or note if nothing new was learned this session).
+```
+- `kebab-case-name` | Brief one-line summary of the fact
+```
+
+Categories: Navigation, UI/Headers, State/Data, Native Modules, TV Platform
+
+## Example
+
+File `.claude/learned-facts/state-management-pattern.md`:
+```markdown
+# State Management Pattern
+
+**Date**: 2025-01-09
+**Category**: state-management
+**Key files**: `utils/atoms/`
+
+## Detail
+
+Use Jotai atoms for global state, NOT React Context. Atoms are defined in `utils/atoms/`.
+```
+
+Index entry in `CLAUDE.md`:
+```
+State/Data:
+- `state-management-pattern` | Use Jotai atoms for global state, not React Context
+```
+
+After updating, summarize what facts you added (or note if nothing new was learned this session).
diff --git a/.claude/learned-facts.md b/.claude/learned-facts.md
index 0dba9d7eb..ab5d6eecc 100644
--- a/.claude/learned-facts.md
+++ b/.claude/learned-facts.md
@@ -1,8 +1,11 @@
-# Learned Facts
+# Learned Facts (DEPRECATED)
-This file contains facts about the codebase learned from past sessions. These are things Claude got wrong or needed clarification on, stored here to prevent the same mistakes in future sessions.
+> **DEPRECATED**: This file has been replaced by individual fact files in `.claude/learned-facts/`.
+> The compressed index is now inline in `CLAUDE.md` under "Learned Facts Index".
+> New facts should be added as individual files using the `/reflect` command.
+> This file is kept for reference only and is no longer auto-imported.
-This file is auto-imported into CLAUDE.md and loaded at the start of each session.
+This file previously contained facts about the codebase learned from past sessions.
## Facts
@@ -24,4 +27,22 @@ 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)_
-- **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)_
\ No newline at end of file
+- **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)_
+
+- **MPV tvOS player exit freeze**: On tvOS, `mpv_terminate_destroy` can deadlock if called while blocking the main thread (e.g., via `queue.sync`). The fix is to run `mpv_terminate_destroy` on `DispatchQueue.global()` asynchronously, allowing it to access main thread for AVFoundation/GPU cleanup. Send `quit` command and drain events first. Located in `modules/mpv-player/ios/MPVLayerRenderer.swift`. _(2026-01-22)_
+
+- **MPV avfoundation-composite-osd ordering**: On tvOS, the `avfoundation-composite-osd` option MUST be set immediately after `vo=avfoundation`, before any `hwdec` options. Skipping or reordering this causes the app to freeze when exiting the player. Set to "no" on tvOS (prevents gray tint), "yes" on iOS (for PiP subtitle support). _(2026-01-22)_
+
+- **Thread-safe state for stop flags**: When using flags like `isStopping` that control loop termination across threads, the setter must be synchronous (`stateQueue.sync`) not async, otherwise the value may not be visible to other threads in time. _(2026-01-22)_
+
+- **TV modals must use navigation pattern**: On TV, never use overlay/absolute-positioned modals (like `TVOptionSelector` at the page level). They don't handle the back button correctly. Always use the navigation-based modal pattern: Jotai atom + hook that calls `router.push()` + page in `app/(auth)/`. Use the existing `useTVOptionModal` hook and `tv-option-modal.tsx` page for option selection. `TVOptionSelector` is only appropriate as a sub-selector *within* a navigation-based modal page. _(2026-01-24)_
+
+- **TV grid layout pattern**: For TV grids, use ScrollView with flexWrap instead of FlatList/FlashList with numColumns. FlatList's numColumns divides width evenly among columns which causes inconsistent item sizing. Use `flexDirection: "row"`, `flexWrap: "wrap"`, `justifyContent: "center"`, and `gap` for spacing. _(2026-01-25)_
+
+- **TV horizontal padding standard**: TV pages should use `TV_HORIZONTAL_PADDING = 60` to match other TV pages like Home, Search, etc. The old `TV_SCALE_PADDING = 20` was too small. _(2026-01-25)_
+
+- **Native SwiftUI view sizing**: When creating Expo native modules with SwiftUI views, the view needs explicit dimensions. Use a `width` prop passed from React Native, set an explicit `.frame(width:height:)` in SwiftUI, and override `intrinsicContentSize` in the ExpoView wrapper to report the correct size to React Native's layout system. Using `.aspectRatio(contentMode: .fit)` alone causes inconsistent sizing. _(2026-01-25)_
+
+- **Streamystats components location**: Streamystats TV components are at `components/home/StreamystatsRecommendations.tv.tsx` and `components/home/StreamystatsPromotedWatchlists.tv.tsx`. The watchlist detail page (which shows items in a grid) is at `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`. _(2026-01-25)_
+
+- **Platform-specific file suffix (.tv.tsx) does NOT work**: The `.tv.tsx` file suffix does NOT work for either pages or components in this project. Metro bundler doesn't resolve platform-specific suffixes. Instead, use `Platform.isTV` conditional rendering within a single file. For pages: check `Platform.isTV` at the top and return the TV component early. For components: create separate `MyComponent.tsx` and `TVMyComponent.tsx` files and use `Platform.isTV` to choose which to render. _(2026-01-26)_
\ No newline at end of file
diff --git a/.claude/learned-facts/header-button-locations.md b/.claude/learned-facts/header-button-locations.md
new file mode 100644
index 000000000..269b51f15
--- /dev/null
+++ b/.claude/learned-facts/header-button-locations.md
@@ -0,0 +1,9 @@
+# Header Button Locations
+
+**Date**: 2026-01-10
+**Category**: ui
+**Key files**: `app/(auth)/(tabs)/(home)/_layout.tsx`, `components/common/HeaderBackButton.tsx`, `components/Chromecast.tsx`, `components/RoundButton.tsx`, `components/home/Home.tsx`, `app/(auth)/(tabs)/(home)/downloads/index.tsx`
+
+## Detail
+
+Header buttons are defined in multiple places: `app/(auth)/(tabs)/(home)/_layout.tsx` (SettingsButton, SessionsButton, back buttons), `components/common/HeaderBackButton.tsx` (reusable), `components/Chromecast.tsx`, `components/RoundButton.tsx`, and dynamically via `navigation.setOptions()` in `components/home/Home.tsx` and `app/(auth)/(tabs)/(home)/downloads/index.tsx`.
diff --git a/.claude/learned-facts/intro-modal-trigger-location.md b/.claude/learned-facts/intro-modal-trigger-location.md
new file mode 100644
index 000000000..4409db06a
--- /dev/null
+++ b/.claude/learned-facts/intro-modal-trigger-location.md
@@ -0,0 +1,9 @@
+# Intro Modal Trigger Location
+
+**Date**: 2025-01-09
+**Category**: navigation
+**Key files**: `components/home/Home.tsx`, `app/(auth)/(tabs)/_layout.tsx`
+
+## Detail
+
+The intro modal trigger logic should be in the `Home.tsx` component, not in the tabs `_layout.tsx`. Triggering modals from tab layout can interfere with native bottom tabs navigation.
diff --git a/.claude/learned-facts/introsheet-rendering-location.md b/.claude/learned-facts/introsheet-rendering-location.md
new file mode 100644
index 000000000..b9575cd72
--- /dev/null
+++ b/.claude/learned-facts/introsheet-rendering-location.md
@@ -0,0 +1,9 @@
+# IntroSheet Rendering Location
+
+**Date**: 2025-01-09
+**Category**: navigation
+**Key files**: `providers/IntroSheetProvider`, `components/IntroSheet`
+
+## Detail
+
+The `IntroSheet` component is rendered inside `IntroSheetProvider` which wraps the entire navigation stack. Any hooks in IntroSheet that interact with navigation state can affect the native bottom tabs.
diff --git a/.claude/learned-facts/macos-header-buttons-fix.md b/.claude/learned-facts/macos-header-buttons-fix.md
new file mode 100644
index 000000000..45d5f31a5
--- /dev/null
+++ b/.claude/learned-facts/macos-header-buttons-fix.md
@@ -0,0 +1,9 @@
+# macOS Header Buttons Fix
+
+**Date**: 2026-01-10
+**Category**: ui
+**Key files**: `components/common/HeaderBackButton.tsx`, `app/(auth)/(tabs)/(home)/_layout.tsx`
+
+## Detail
+
+Header buttons (`headerRight`/`headerLeft`) don't respond to touches on macOS Catalyst builds when using standard React Native `TouchableOpacity`. Fix by using `Pressable` from `react-native-gesture-handler` instead. The library is already installed and `GestureHandlerRootView` wraps the app.
diff --git a/.claude/learned-facts/mark-as-played-flow.md b/.claude/learned-facts/mark-as-played-flow.md
new file mode 100644
index 000000000..48603cd0c
--- /dev/null
+++ b/.claude/learned-facts/mark-as-played-flow.md
@@ -0,0 +1,9 @@
+# Mark as Played Flow
+
+**Date**: 2026-01-10
+**Category**: state-management
+**Key files**: `components/PlayedStatus.tsx`, `hooks/useMarkAsPlayed.ts`
+
+## Detail
+
+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`.
diff --git a/.claude/learned-facts/mpv-avfoundation-composite-osd-ordering.md b/.claude/learned-facts/mpv-avfoundation-composite-osd-ordering.md
new file mode 100644
index 000000000..418f862ae
--- /dev/null
+++ b/.claude/learned-facts/mpv-avfoundation-composite-osd-ordering.md
@@ -0,0 +1,9 @@
+# MPV avfoundation-composite-osd Ordering
+
+**Date**: 2026-01-22
+**Category**: native-modules
+**Key files**: `modules/mpv-player/ios/MPVLayerRenderer.swift`
+
+## Detail
+
+On tvOS, the `avfoundation-composite-osd` option MUST be set immediately after `vo=avfoundation`, before any `hwdec` options. Skipping or reordering this causes the app to freeze when exiting the player. Set to "no" on tvOS (prevents gray tint), "yes" on iOS (for PiP subtitle support).
diff --git a/.claude/learned-facts/mpv-tvos-player-exit-freeze.md b/.claude/learned-facts/mpv-tvos-player-exit-freeze.md
new file mode 100644
index 000000000..7dfb20178
--- /dev/null
+++ b/.claude/learned-facts/mpv-tvos-player-exit-freeze.md
@@ -0,0 +1,9 @@
+# MPV tvOS Player Exit Freeze
+
+**Date**: 2026-01-22
+**Category**: native-modules
+**Key files**: `modules/mpv-player/ios/MPVLayerRenderer.swift`
+
+## Detail
+
+On tvOS, `mpv_terminate_destroy` can deadlock if called while blocking the main thread (e.g., via `queue.sync`). The fix is to run `mpv_terminate_destroy` on `DispatchQueue.global()` asynchronously, allowing it to access main thread for AVFoundation/GPU cleanup. Send `quit` command and drain events first.
diff --git a/.claude/learned-facts/native-bottom-tabs-userouter-conflict.md b/.claude/learned-facts/native-bottom-tabs-userouter-conflict.md
new file mode 100644
index 000000000..eda49ef05
--- /dev/null
+++ b/.claude/learned-facts/native-bottom-tabs-userouter-conflict.md
@@ -0,0 +1,9 @@
+# Native Bottom Tabs + useRouter Conflict
+
+**Date**: 2025-01-09
+**Category**: navigation
+**Key files**: `providers/`, `app/_layout.tsx`
+
+## Detail
+
+When using `@bottom-tabs/react-navigation` with Expo Router, avoid using the `useRouter()` hook in components rendered at the provider level (outside the tab navigator). The hook subscribes to navigation state changes and can cause unexpected tab switches. Use the static `router` import from `expo-router` instead.
diff --git a/.claude/learned-facts/native-swiftui-view-sizing.md b/.claude/learned-facts/native-swiftui-view-sizing.md
new file mode 100644
index 000000000..f36a18374
--- /dev/null
+++ b/.claude/learned-facts/native-swiftui-view-sizing.md
@@ -0,0 +1,9 @@
+# Native SwiftUI View Sizing
+
+**Date**: 2026-01-25
+**Category**: native-modules
+**Key files**: `modules/`
+
+## Detail
+
+When creating Expo native modules with SwiftUI views, the view needs explicit dimensions. Use a `width` prop passed from React Native, set an explicit `.frame(width:height:)` in SwiftUI, and override `intrinsicContentSize` in the ExpoView wrapper to report the correct size to React Native's layout system. Using `.aspectRatio(contentMode: .fit)` alone causes inconsistent sizing.
diff --git a/.claude/learned-facts/platform-specific-file-suffix-does-not-work.md b/.claude/learned-facts/platform-specific-file-suffix-does-not-work.md
new file mode 100644
index 000000000..d52dca9b9
--- /dev/null
+++ b/.claude/learned-facts/platform-specific-file-suffix-does-not-work.md
@@ -0,0 +1,9 @@
+# Platform-Specific File Suffix (.tv.tsx) Does NOT Work
+
+**Date**: 2026-01-26
+**Category**: tv
+**Key files**: `app/`, `components/`
+
+## Detail
+
+The `.tv.tsx` file suffix does NOT work for either pages or components in this project. Metro bundler doesn't resolve platform-specific suffixes. Instead, use `Platform.isTV` conditional rendering within a single file. For pages: check `Platform.isTV` at the top and return the TV component early. For components: create separate `MyComponent.tsx` and `TVMyComponent.tsx` files and use `Platform.isTV` to choose which to render.
diff --git a/.claude/learned-facts/stack-screen-header-configuration.md b/.claude/learned-facts/stack-screen-header-configuration.md
new file mode 100644
index 000000000..24ca01fcd
--- /dev/null
+++ b/.claude/learned-facts/stack-screen-header-configuration.md
@@ -0,0 +1,9 @@
+# Stack Screen Header Configuration
+
+**Date**: 2026-01-10
+**Category**: ui
+**Key files**: `app/(auth)/(tabs)/(home)/_layout.tsx`
+
+## Detail
+
+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.
diff --git a/.claude/learned-facts/streamystats-components-location.md b/.claude/learned-facts/streamystats-components-location.md
new file mode 100644
index 000000000..41652a528
--- /dev/null
+++ b/.claude/learned-facts/streamystats-components-location.md
@@ -0,0 +1,9 @@
+# Streamystats Components Location
+
+**Date**: 2026-01-25
+**Category**: tv
+**Key files**: `components/home/StreamystatsRecommendations.tv.tsx`, `components/home/StreamystatsPromotedWatchlists.tv.tsx`, `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`
+
+## Detail
+
+Streamystats TV components are at `components/home/StreamystatsRecommendations.tv.tsx` and `components/home/StreamystatsPromotedWatchlists.tv.tsx`. The watchlist detail page (which shows items in a grid) is at `app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx`.
diff --git a/.claude/learned-facts/tab-folder-naming.md b/.claude/learned-facts/tab-folder-naming.md
new file mode 100644
index 000000000..7663a6099
--- /dev/null
+++ b/.claude/learned-facts/tab-folder-naming.md
@@ -0,0 +1,9 @@
+# Tab Folder Naming
+
+**Date**: 2025-01-09
+**Category**: navigation
+**Key files**: `app/(auth)/(tabs)/`
+
+## Detail
+
+The tab folders use underscore prefix naming like `(_home)` instead of just `(home)` based on the project's file structure conventions.
diff --git a/.claude/learned-facts/thread-safe-state-for-stop-flags.md b/.claude/learned-facts/thread-safe-state-for-stop-flags.md
new file mode 100644
index 000000000..eaa0d84d0
--- /dev/null
+++ b/.claude/learned-facts/thread-safe-state-for-stop-flags.md
@@ -0,0 +1,9 @@
+# Thread-Safe State for Stop Flags
+
+**Date**: 2026-01-22
+**Category**: native-modules
+**Key files**: `modules/mpv-player/ios/MPVLayerRenderer.swift`
+
+## Detail
+
+When using flags like `isStopping` that control loop termination across threads, the setter must be synchronous (`stateQueue.sync`) not async, otherwise the value may not be visible to other threads in time.
diff --git a/.claude/learned-facts/tv-grid-layout-pattern.md b/.claude/learned-facts/tv-grid-layout-pattern.md
new file mode 100644
index 000000000..6f9b234a2
--- /dev/null
+++ b/.claude/learned-facts/tv-grid-layout-pattern.md
@@ -0,0 +1,9 @@
+# TV Grid Layout Pattern
+
+**Date**: 2026-01-25
+**Category**: tv
+**Key files**: `components/tv/`
+
+## Detail
+
+For TV grids, use ScrollView with flexWrap instead of FlatList/FlashList with numColumns. FlatList's numColumns divides width evenly among columns which causes inconsistent item sizing. Use `flexDirection: "row"`, `flexWrap: "wrap"`, `justifyContent: "center"`, and `gap` for spacing.
diff --git a/.claude/learned-facts/tv-horizontal-padding-standard.md b/.claude/learned-facts/tv-horizontal-padding-standard.md
new file mode 100644
index 000000000..e9ddc0c88
--- /dev/null
+++ b/.claude/learned-facts/tv-horizontal-padding-standard.md
@@ -0,0 +1,9 @@
+# TV Horizontal Padding Standard
+
+**Date**: 2026-01-25
+**Category**: tv
+**Key files**: `components/tv/`, `app/(auth)/(tabs)/`
+
+## Detail
+
+TV pages should use `TV_HORIZONTAL_PADDING = 60` to match other TV pages like Home, Search, etc. The old `TV_SCALE_PADDING = 20` was too small.
diff --git a/.claude/learned-facts/tv-modals-must-use-navigation-pattern.md b/.claude/learned-facts/tv-modals-must-use-navigation-pattern.md
new file mode 100644
index 000000000..c6c837d5a
--- /dev/null
+++ b/.claude/learned-facts/tv-modals-must-use-navigation-pattern.md
@@ -0,0 +1,9 @@
+# TV Modals Must Use Navigation Pattern
+
+**Date**: 2026-01-24
+**Category**: tv
+**Key files**: `hooks/useTVOptionModal.ts`, `app/(auth)/tv-option-modal.tsx`
+
+## Detail
+
+On TV, never use overlay/absolute-positioned modals (like `TVOptionSelector` at the page level). They don't handle the back button correctly. Always use the navigation-based modal pattern: Jotai atom + hook that calls `router.push()` + page in `app/(auth)/`. Use the existing `useTVOptionModal` hook and `tv-option-modal.tsx` page for option selection. `TVOptionSelector` is only appropriate as a sub-selector *within* a navigation-based modal page.
diff --git a/.claude/learned-facts/use-network-aware-query-client-limitations.md b/.claude/learned-facts/use-network-aware-query-client-limitations.md
new file mode 100644
index 000000000..36e8f2d82
--- /dev/null
+++ b/.claude/learned-facts/use-network-aware-query-client-limitations.md
@@ -0,0 +1,9 @@
+# useNetworkAwareQueryClient Limitations
+
+**Date**: 2026-01-10
+**Category**: state-management
+**Key files**: `hooks/useNetworkAwareQueryClient.ts`
+
+## Detail
+
+The `useNetworkAwareQueryClient` hook uses `Object.create(queryClient)` which breaks QueryClient methods that use JavaScript private fields (like `getQueriesData`, `setQueriesData`, `setQueryData`). Only use it when you ONLY need `invalidateQueries`. For cache manipulation, use standard `useQueryClient` from `@tanstack/react-query`.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index e76ebb70d..95978bab3 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,91 +1,54 @@
-
# 📦 Pull Request
-## 🔖 Summary
+
+
+
+## 📝 Description
## 🏷️ Ticket / Issue
-## 🛠️ What’s Changed
-
-
-- Type: feat | fix | docs | style | refactor | perf | test | chore | build | ci | revert
-- Scope (optional): e.g., auth, billing, mobile
-- Short summary: what changed and why (1–2 lines)
--->
-
-## 📋 Details
-
-
-### ⚠️ Breaking Changes
-
-
-### 🔐 Security & Privacy Impact
-
-
-### ⚡ Performance Impact
-
-
### 🖼️ Screenshots / GIFs (if UI)
-
+
## ✅ Checklist
- [ ] I’ve read the [contribution guidelines](CONTRIBUTING.md)
-- [ ] Code follows project style and passes lint/format (`npm|pnpm|yarn|bun` scripts)
-- [ ] Type checks pass (tsc/biome/etc.)
-- [ ] Docs updated (README/ADR/usage/API)
-- [ ] No secrets/credentials included; env vars documented
-- [ ] Release notes/CHANGELOG entry added (if applicable)
-- [ ] Verified locally that changes behave as expected
+- [ ] Verified that changes behave as expected for all platforms
+- [ ] Code passes lint/formatting and type checks (`tsc`/`biome`)
+- [ ] No secrets, hardcoded credentials, or private config files are included
+- [ ] I've declared if AI was used to assist with this PR (by uncommenting the line at the bottom, or not)
## 🔍 Testing Instructions
-## ⚙️ Deployment Notes
-
-
-## 📝 Additional Notes
-
\ No newline at end of file
diff --git a/.github/renovate.json b/.github/renovate.json
index 364842311..fdbe3734d 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -25,6 +25,25 @@
"osvVulnerabilityAlerts": true,
"configMigration": true,
"separateMinorPatch": true,
+ "customManagers": [
+ {
+ "customType": "regex",
+ "managerFilePatterns": ["/\\.ya?ml$/"],
+ "matchStrings": [
+ "# renovate: datasource=(?\\S+) depName=(?\\S+)(?: versioning=(?\\S+))?\\s+xcode-version:\\s*[\"']?(?[^\"'\\s]+)"
+ ],
+ "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}loose{{/if}}"
+ }
+ ],
+ "customDatasources": {
+ "xcode": {
+ "defaultRegistryUrlTemplate": "https://xcodereleases.com/data.json",
+ "format": "json",
+ "transformTemplates": [
+ "{ \"releases\": [$[version.release.release=true].{\"version\": version.number}] }"
+ ]
+ }
+ },
"lockFileMaintenance": {
"vulnerabilityAlerts": {
"enabled": true,
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 8528a95bd..8c7d53313 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -26,7 +26,7 @@ jobs:
steps:
- name: 🔍 Get PR and Artifacts
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
// Check if we're running from a fork (more precise detection)
@@ -188,6 +188,17 @@ jobs:
if (latestAppsRun) {
console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
+ // Map job names to our build targets. Declared outside the try so
+ // the catch fallback can reuse the same keys.
+ const jobMappings = {
+ 'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
+ 'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
+ 'iOS': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone'],
+ 'iOS Unsigned': ['🍎 Build iOS IPA (Phone - Unsigned)', 'build-ios-phone-unsigned'],
+ 'tvOS': ['🍎 Build tvOS IPA', 'build-ios-tv'],
+ 'tvOS Unsigned': ['🍎 Build tvOS IPA (Unsigned)', 'build-ios-tv-unsigned']
+ };
+
try {
// Get all jobs for this workflow run
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
@@ -216,13 +227,6 @@ jobs:
return; // Exit early
}
- // Map job names to our build targets
- const jobMappings = {
- 'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
- 'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
- 'iOS Phone': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone']
- };
-
// Create individual status for each job
for (const [platform, jobNames] of Object.entries(jobMappings)) {
const job = jobs.jobs.find(j =>
@@ -236,7 +240,9 @@ jobs:
conclusion: job.conclusion,
url: job.html_url,
runId: latestAppsRun.id,
- created_at: job.started_at || latestAppsRun.created_at
+ created_at: job.started_at || latestAppsRun.created_at,
+ started_at: job.started_at,
+ completed_at: job.completed_at
};
console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`);
} else {
@@ -247,22 +253,30 @@ jobs:
conclusion: latestAppsRun.conclusion,
url: latestAppsRun.html_url,
runId: latestAppsRun.id,
- created_at: latestAppsRun.created_at
+ created_at: latestAppsRun.created_at,
+ started_at: latestAppsRun.run_started_at,
+ completed_at: latestAppsRun.updated_at
};
}
}
} catch (error) {
console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message);
- // Fallback to workflow-level status
- buildStatuses['Android Phone'] = buildStatuses['Android TV'] = buildStatuses['iOS Phone'] = {
+ // Fallback to workflow-level status for every build target.
+ // Keys must match jobMappings / buildTargets statusKey values.
+ const fallbackStatus = {
name: latestAppsRun.name,
status: latestAppsRun.status,
conclusion: latestAppsRun.conclusion,
url: latestAppsRun.html_url,
runId: latestAppsRun.id,
- created_at: latestAppsRun.created_at
+ created_at: latestAppsRun.created_at,
+ started_at: latestAppsRun.run_started_at,
+ completed_at: latestAppsRun.updated_at
};
+ for (const platform of Object.keys(jobMappings)) {
+ buildStatuses[platform] = fallbackStatus;
+ }
}
// Collect artifacts if any job has completed successfully
@@ -353,10 +367,12 @@ jobs:
// Process each expected build target individually
const buildTargets = [
- { name: 'Android Phone', platform: '🤖', device: '📱', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
- { name: 'Android TV', platform: '🤖', device: '📺', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
- { name: 'iOS Phone', platform: '🍎', device: '📱', statusKey: 'iOS Phone', artifactPattern: /ios.*phone/i },
- { name: 'iOS TV', platform: '🍎', device: '📺', statusKey: 'iOS TV', artifactPattern: /ios.*tv/i }
+ { name: 'Android Phone', platform: '🤖', device: '📱 Phone', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
+ { name: 'Android TV', platform: '🤖', device: '📺 TV', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
+ { name: 'iOS', platform: '🍎', device: '📱 Phone', statusKey: 'iOS', artifactPattern: /ios.*phone.*ipa(?!.*unsigned)/i },
+ { name: 'iOS Unsigned', platform: '🍎', device: '📱 Phone Unsigned', statusKey: 'iOS Unsigned', artifactPattern: /ios.*phone.*unsigned/i },
+ { name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /ios.*tv.*ipa(?!.*unsigned)/i },
+ { name: 'tvOS Unsigned', platform: '🍎', device: '📺 TV Unsigned', statusKey: 'tvOS Unsigned', artifactPattern: /ios.*tv.*unsigned/i }
];
for (const target of buildTargets) {
@@ -371,16 +387,31 @@ jobs:
let status = '⏳ Pending';
let downloadLink = '*Waiting for build...*';
- // Special case for iOS TV - show as disabled
- if (target.name === 'iOS TV') {
+ // tvOS builds are temporarily disabled until feat/tv-interface
+ // is merged - show them as disabled instead of stuck pending.
+ if (target.name === 'tvOS' || target.name === 'tvOS Unsigned') {
status = '💤 Disabled';
- downloadLink = '*Disabled for now*';
+ downloadLink = '*Disabled until feat/tv-interface is merged*';
} else if (matchingStatus) {
if (matchingStatus.conclusion === 'success' && matchingArtifact) {
status = '✅ Complete';
const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`;
const fileType = target.name.includes('Android') ? 'APK' : 'IPA';
- downloadLink = `[📥 Download ${fileType}](${directLink})`;
+
+ // Format file size
+ const sizeInMB = (matchingArtifact.size_in_bytes / (1024 * 1024)).toFixed(1);
+ const sizeInfo = `(${sizeInMB} MB)`;
+
+ // Calculate build duration
+ let durationInfo = '';
+ if (matchingStatus.started_at && matchingStatus.completed_at) {
+ const durationMs = new Date(matchingStatus.completed_at) - new Date(matchingStatus.started_at);
+ const durationMin = Math.floor(durationMs / 60000);
+ const durationSec = Math.floor((durationMs % 60000) / 1000);
+ durationInfo = ` - ${durationMin}m ${durationSec}s`;
+ }
+
+ downloadLink = `[📥 Download ${fileType}](${directLink}) ${sizeInfo}${durationInfo}`;
} else if (matchingStatus.conclusion === 'failure') {
status = `❌ [Failed](${matchingStatus.url})`;
downloadLink = '*Build failed*';
@@ -408,7 +439,7 @@ jobs:
}
}
- commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`;
+ commentBody += `| ${target.platform} ${target.name} | ${target.device} | ${status} | ${downloadLink} |\n`;
}
commentBody += `\n`;
diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml
index b300c04d4..fd68e23a1 100644
--- a/.github/workflows/build-apps.yml
+++ b/.github/workflows/build-apps.yml
@@ -41,12 +41,12 @@ jobs:
show-progress: false
- name: 🍞 Setup Bun
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: 💾 Cache Bun dependencies
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
@@ -60,7 +60,7 @@ jobs:
bun run submodule-reload
- name: 💾 Cache Gradle global
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
@@ -73,7 +73,7 @@ jobs:
run: bun run prebuild
- name: 💾 Cache project Gradle (.gradle)
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: android/.gradle
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
@@ -88,7 +88,7 @@ jobs:
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload APK artifact
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: streamyfin-android-phone-apk-${{ env.DATE_TAG }}
path: |
@@ -124,12 +124,12 @@ jobs:
show-progress: false
- name: 🍞 Setup Bun
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: 💾 Cache Bun dependencies
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
@@ -143,7 +143,7 @@ jobs:
bun run submodule-reload
- name: 💾 Cache Gradle global
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
@@ -156,7 +156,7 @@ jobs:
run: bun run prebuild:tv
- name: 💾 Cache project Gradle (.gradle)
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: android/.gradle
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
@@ -171,7 +171,7 @@ jobs:
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
- name: 📤 Upload APK artifact
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: streamyfin-android-tv-apk-${{ env.DATE_TAG }}
path: |
@@ -195,12 +195,12 @@ jobs:
show-progress: false
- name: 🍞 Setup Bun
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: 💾 Cache Bun dependencies
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
@@ -216,12 +216,13 @@ jobs:
run: bun run prebuild
- name: 🔧 Setup Xcode
- uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
- xcode-version: "26.2"
+ # renovate: datasource=custom.xcode depName=xcode versioning=loose
+ xcode-version: "26.4"
- name: 🏗️ Setup EAS
- uses: expo/expo-github-action@main
+ uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
@@ -236,7 +237,7 @@ jobs:
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
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: streamyfin-ios-phone-ipa-${{ env.DATE_TAG }}
path: build-*.ipa
@@ -259,12 +260,12 @@ jobs:
show-progress: false
- name: 🍞 Setup Bun
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: 💾 Cache Bun dependencies
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
@@ -280,9 +281,10 @@ jobs:
run: bun run prebuild
- name: 🔧 Setup Xcode
- uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
- xcode-version: "26.2"
+ # renovate: datasource=custom.xcode depName=xcode versioning=loose
+ xcode-version: "26.4"
- name: 🚀 Build iOS app
env:
@@ -293,73 +295,136 @@ jobs:
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
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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
- # 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'))
- # runs-on: macos-26
- # name: 🍎 Build iOS IPA (TV)
- # permissions:
- # contents: read
- #
- # steps:
- # - name: 📥 Checkout code
- # uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- # 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@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
- # with:
- # bun-version: latest
- #
- # - name: 💾 Cache Bun dependencies
- # uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- # 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:tv
- #
- # - name: 🔧 Setup Xcode
- # uses: maxim-lobanov/setup-xcode@v1
- # with:
- # xcode-version: '26.0.1'
- #
- # - name: 🏗️ Setup EAS
- # uses: expo/expo-github-action@main
- # with:
- # eas-version: latest
- # token: ${{ secrets.EXPO_TOKEN }}
- # eas-cache: true
- #
- # - name: 🚀 Build iOS app
- # env:
- # EXPO_TV: 1
- # run: eas build -p ios --local --non-interactive
- #
- # - 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- # with:
- # name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
- # path: build-*.ipa
- # retention-days: 7
+ build-ios-tv:
+ # Temporarily disabled until feat/tv-interface is merged (TV UI not ready).
+ # Re-enable by removing the `false &&` prefix below.
+ if: false && (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
+ runs-on: macos-26
+ name: 🍎 Build tvOS IPA
+ permissions:
+ contents: read
+
+ steps:
+ - name: 📥 Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ 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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
+ with:
+ bun-version: latest
+
+ - name: 💾 Cache Bun dependencies
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ 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:tv
+
+ - name: 🔧 Setup Xcode
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
+ with:
+ # renovate: datasource=custom.xcode depName=xcode versioning=loose
+ xcode-version: "26.4"
+
+ - name: 🏗️ Setup EAS
+ uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main
+ with:
+ eas-version: latest
+ token: ${{ secrets.EXPO_TOKEN }}
+ eas-cache: true
+
+ - name: 🚀 Build iOS app
+ env:
+ EXPO_TV: 1
+ run: eas build -p ios --local --non-interactive
+
+ - 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
+ path: build-*.ipa
+ retention-days: 7
+
+ build-ios-tv-unsigned:
+ # Unsigned tvOS build is enabled (compiles without Apple credentials).
+ # The signed tvOS job above stays disabled until tvOS provisioning
+ # profiles are set up in EAS (app + TopShelf targets).
+ if: (!contains(github.event.head_commit.message, '[skip ci]'))
+ runs-on: macos-26
+ name: 🍎 Build tvOS IPA (Unsigned)
+ permissions:
+ contents: read
+
+ steps:
+ - name: 📥 Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ 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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
+ with:
+ bun-version: latest
+
+ - name: 💾 Cache Bun dependencies
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ 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:tv
+
+ - name: 🔧 Setup Xcode
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
+ with:
+ # renovate: datasource=custom.xcode depName=xcode versioning=loose
+ xcode-version: "26.4"
+
+ - name: 🚀 Build iOS app
+ env:
+ EXPO_TV: 1
+ run: bun run ios:unsigned-build:tv ${{ 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: streamyfin-ios-tv-unsigned-ipa-${{ env.DATE_TAG }}
+ path: build/*.ipa
+ retention-days: 7
diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml
index ad189f7f3..ae4c0fe02 100644
--- a/.github/workflows/check-lockfile.yml
+++ b/.github/workflows/check-lockfile.yml
@@ -27,12 +27,12 @@ jobs:
fetch-depth: 0
- name: 🍞 Setup Bun
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- name: 💾 Cache Bun dependencies
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.bun/install/cache
diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml
index 29330ac7d..ba1c08dc8 100644
--- a/.github/workflows/ci-codeql.yml
+++ b/.github/workflows/ci-codeql.yml
@@ -27,13 +27,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 🏁 Initialize CodeQL
- uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
+ uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality
- name: 🛠️ Autobuild
- uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
+ uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
- name: 🧪 Perform CodeQL Analysis
- uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
+ uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml
index c73cad706..c6effebf1 100644
--- a/.github/workflows/crowdin.yml
+++ b/.github/workflows/crowdin.yml
@@ -28,7 +28,7 @@ jobs:
fetch-depth: 0
- name: 🌐 Sync Translations with Crowdin
- uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0
+ uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
with:
upload_sources: true
upload_translations: true
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index 08e0a8847..50013ba2b 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -25,7 +25,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
+ - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
@@ -39,7 +39,7 @@ jobs:
```
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
- uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
+ uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: pr-title-lint-error
delete: true
@@ -57,7 +57,7 @@ jobs:
fetch-depth: 0
- name: Dependency Review
- uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
+ uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
base-ref: ${{ github.event.pull_request.base.sha || 'develop' }}
@@ -76,7 +76,7 @@ jobs:
fetch-depth: 0
- name: 🍞 Setup Bun
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
@@ -107,12 +107,12 @@ jobs:
fetch-depth: 0
- name: "🟢 Setup Node.js"
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24.x'
- name: "🍞 Setup Bun"
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml
index 25fa33196..7cc321977 100644
--- a/.github/workflows/update-issue-form.yml
+++ b/.github/workflows/update-issue-form.yml
@@ -21,14 +21,14 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "🟢 Setup Node.js"
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24.x'
cache: 'npm'
- name: 🔍 Extract minor version from app.json
id: minor
- uses: actions/github-script@main
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # main
with:
result-encoding: string
script: |
@@ -54,7 +54,7 @@ jobs:
dry_run: no-push
- name: 📬 Commit and create pull request
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
+ uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
add-paths: .github/ISSUE_TEMPLATE/bug_report.yml
branch: ci-update-bug-report
diff --git a/.gitignore b/.gitignore
index 2b3bd6ed7..c39e191b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,8 +50,6 @@ npm-debug.*
.idea/
.ruby-lsp
.cursor/
-.claude/
-CLAUDE.md
# Environment and Configuration
expo-env.d.ts
@@ -63,6 +61,8 @@ expo-env.d.ts
pc-api-7079014811501811218-719-3b9f15aeccf8.json
credentials.json
streamyfin-4fec1-firebase-adminsdk.json
+/profiles/
+certs/
# Version and Backup Files
/version-backup-*
@@ -72,4 +72,7 @@ modules/background-downloader/android/build/*
/modules/mpv-player/android/build
# ios:unsigned-build Artifacts
-build/
\ No newline at end of file
+build/
+.claude/
+.agents/skills/**
+skills-lock.json
diff --git a/CLAUDE.md b/CLAUDE.md
index cc3b0a53d..eb2ae87e5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,9 +1,39 @@
# CLAUDE.md
-@.claude/learned-facts.md
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+## Learned Facts Index
+
+IMPORTANT: When encountering issues related to these topics, or when implementing new features that touch these areas, prefer retrieval-led reasoning -- read the relevant fact file in `.claude/learned-facts/` before relying on assumptions.
+
+Navigation:
+- `native-bottom-tabs-userouter-conflict` | useRouter() at provider level causes tab switches; use static router import
+- `introsheet-rendering-location` | IntroSheet in IntroSheetProvider affects native bottom tabs via nav state hooks
+- `intro-modal-trigger-location` | Trigger in Home.tsx, not tabs _layout.tsx
+- `tab-folder-naming` | Use underscore prefix: (_home) not (home)
+
+UI/Headers:
+- `macos-header-buttons-fix` | macOS Catalyst: use RNGH Pressable, not RN TouchableOpacity
+- `header-button-locations` | Defined in _layout.tsx, HeaderBackButton, Chromecast, RoundButton, etc.
+- `stack-screen-header-configuration` | Sub-pages need explicit Stack.Screen with headerTransparent + back button
+
+State/Data:
+- `use-network-aware-query-client-limitations` | Object.create breaks private fields; only for invalidateQueries
+- `mark-as-played-flow` | PlayedStatus→useMarkAsPlayed→playbackManager with optimistic updates
+
+Native Modules:
+- `mpv-tvos-player-exit-freeze` | mpv_terminate_destroy deadlocks main thread; use DispatchQueue.global()
+- `mpv-avfoundation-composite-osd-ordering` | MUST follow vo=avfoundation, before hwdec options
+- `thread-safe-state-for-stop-flags` | Stop flags need synchronous setter (stateQueue.sync not async)
+- `native-swiftui-view-sizing` | Need explicit frame + intrinsicContentSize override in ExpoView
+
+TV Platform:
+- `tv-modals-must-use-navigation-pattern` | Use atom+router.push(), never overlay/absolute modals
+- `tv-grid-layout-pattern` | ScrollView+flexWrap, not FlatList numColumns
+- `tv-horizontal-padding-standard` | TV_HORIZONTAL_PADDING=60, not old TV_SCALE_PADDING=20
+- `streamystats-components-location` | components/home/Streamystats*.tv.tsx, watchlists/[watchlistId].tsx
+- `platform-specific-file-suffix-does-not-work` | .tv.tsx doesn't work; use Platform.isTV conditional rendering
+
## Project Overview
Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). It supports mobile (iOS/Android) and TV platforms, with features including offline downloads, Chromecast support, and Jellyseerr integration.
@@ -65,6 +95,7 @@ bun run ios:install-metal-toolchain # Fix "missing Metal Toolchain" build error
**State Management**:
- Global state uses Jotai atoms in `utils/atoms/`
- `settingsAtom` in `utils/atoms/settings.ts` for app settings
+ - **IMPORTANT**: When adding a setting to the settings atom, ensure it's toggleable in the settings view (either TV or mobile, depending on the feature scope)
- `apiAtom` and `userAtom` in `providers/JellyfinProvider.tsx` for auth state
- Server state uses React Query with `@tanstack/react-query`
@@ -128,9 +159,132 @@ import { apiAtom } from "@/providers/JellyfinProvider";
- Handle both mobile and TV navigation patterns
- Use existing atoms, hooks, and utilities before creating new ones
- Use Conventional Commits: `feat(scope):`, `fix(scope):`, `chore(scope):`
+- **Translations**: When adding a translation key to a Text component, ensure the key exists in both `translations/en.json` and `translations/sv.json`. Before adding new keys, check if an existing key already covers the use case.
## Platform Considerations
- TV version uses `:tv` suffix for scripts
- Platform checks: `Platform.isTV`, `Platform.OS === "android"` or `"ios"`
- Some features disabled on TV (e.g., notifications, Chromecast)
+- **TV Design**: Don't use purple accent colors on TV. Use white for focused states and `expo-blur` (`BlurView`) for backgrounds/overlays.
+- **TV Typography**: Use `TVTypography` from `@/components/tv/TVTypography` for all text on TV. It provides consistent font sizes optimized for TV viewing distance.
+- **TV Button Sizing**: Ensure buttons placed next to each other have the same size for visual consistency.
+- **TV Focus Scale Padding**: Add sufficient padding around focusable items in tables/rows/columns/lists. The focus scale animation (typically 1.05x) will clip against parent containers without proper padding. Use `overflow: "visible"` on containers and add padding to prevent clipping.
+- **TV Modals**: Never use React Native's `Modal` component or overlay/absolute-positioned modals for full-screen modals on TV. Use the navigation-based modal pattern instead. **See [docs/tv-modal-guide.md](docs/tv-modal-guide.md) for detailed documentation.**
+
+### TV Component Rendering Pattern
+
+**IMPORTANT**: The `.tv.tsx` file suffix does NOT work in this project - neither for pages nor components. Metro bundler doesn't resolve platform-specific suffixes. Always use `Platform.isTV` conditional rendering instead.
+
+**Pattern for TV-specific pages and components**:
+```typescript
+// In page file (e.g., app/login.tsx)
+import { Platform } from "react-native";
+import { Login } from "@/components/login/Login";
+import { TVLogin } from "@/components/login/TVLogin";
+
+const LoginPage: React.FC = () => {
+ if (Platform.isTV) {
+ return ;
+ }
+ return ;
+};
+
+export default LoginPage;
+```
+
+- Create separate component files for mobile and TV (e.g., `MyComponent.tsx` and `TVMyComponent.tsx`)
+- Use `Platform.isTV` to conditionally render the appropriate component
+- TV components typically use `TVInput`, `TVServerCard`, and other TV-prefixed components with focus handling
+- **Never use `.tv.tsx` file suffix** - it will not be resolved correctly
+
+### TV Option Selectors and Focus Management
+
+For dropdown/select components, bottom sheets, and overlay focus management on TV, see [docs/tv-modal-guide.md](docs/tv-modal-guide.md).
+
+### TV Focus Flickering Between Zones (Lists with Headers)
+
+When you have a page with multiple focusable zones (e.g., a filter bar above a grid), the TV focus engine can rapidly flicker between elements when navigating between zones. This is a known issue with React Native TV.
+
+**Solutions:**
+
+1. **Use FlatList instead of FlashList for TV** - FlashList has known focus issues on TV platforms. Use regular FlatList with `Platform.isTV` check:
+```typescript
+{Platform.isTV ? (
+
+) : (
+
+)}
+```
+
+2. **Add `removeClippedSubviews={false}`** - Prevents the list from unmounting off-screen items, which can cause focus to "fall through" to other elements.
+
+3. **Only ONE element should have `hasTVPreferredFocus`** - Never have multiple elements competing for initial focus. Choose one element (usually the first filter button or first list item) to have preferred focus:
+```typescript
+// ✅ Good - only first filter button has preferred focus
+
+ // No hasTVPreferredFocus
+
+// ❌ Bad - both compete for focus
+
+
+```
+
+4. **Keep headers/filter bars outside the list** - Instead of using `ListHeaderComponent`, render the filter bar as a separate View above the FlatList:
+```typescript
+
+ {/* Filter bar - separate from list */}
+
+
+
+
+
+ {/* Grid */}
+
+
+```
+
+5. **Avoid multiple scrollable containers** - Don't use ScrollView for the filter bar if you have a FlatList below. Use a simple View instead to prevent focus conflicts between scrollable containers.
+
+**Reference implementation**: See `app/(auth)/(tabs)/(libraries)/[libraryId].tsx` for the TV filter bar + grid pattern.
+
+### TV Focus Guide Navigation (Non-Adjacent Sections)
+
+When you need focus to navigate between sections that aren't geometrically aligned (e.g., left-aligned buttons to a horizontal ScrollView), use `TVFocusGuideView` with the `destinations` prop:
+
+```typescript
+// 1. Track destination with useState (NOT useRef - won't trigger re-renders)
+const [firstCardRef, setFirstCardRef] = useState(null);
+
+// 2. Place invisible focus guide between sections
+{firstCardRef && (
+
+)}
+
+// 3. Target component must use forwardRef
+const MyCard = React.forwardRef(({ ... }, ref) => (
+
+ ...
+
+));
+
+// 4. Pass state setter as callback ref to first item
+{items.map((item, index) => (
+
+))}
+```
+
+**For detailed documentation and bidirectional navigation patterns, see [docs/tv-focus-guide.md](docs/tv-focus-guide.md)**
+
+**Reference implementation**: See `components/ItemContent.tv.tsx` for bidirectional focus navigation between playback options and cast list.
diff --git a/README.md b/README.md
index b5b418d21..258005ef7 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,10 @@ For the TV version suffix the npm commands with `:tv`.
`npm run prebuild:tv`
`npm run ios:tv or npm run android:tv`
+TV platform integration notes:
+
+- [TV Discovery](./docs/tv-discovery.md)
+
## 👋 Get in Touch with Us
Need assistance or have any questions?
diff --git a/app.config.js b/app.config.js
index 2e37927bc..96bbd8ea0 100644
--- a/app.config.js
+++ b/app.config.js
@@ -6,6 +6,14 @@ module.exports = ({ config }) => {
"react-native-google-cast",
{ useDefaultExpandedMediaControls: true },
]);
+
+ config.plugins.push([
+ "expo-camera",
+ {
+ cameraPermission:
+ "Allow Streamyfin to access the camera to scan QR codes for TV login.",
+ },
+ ]);
}
// Only override googleServicesFile if env var is set
diff --git a/app.json b/app.json
index 288f3e3af..6dee6c85a 100644
--- a/app.json
+++ b/app.json
@@ -2,13 +2,11 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
- "version": "0.52.0",
+ "version": "0.54.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
"userInterfaceStyle": "dark",
- "jsEngine": "hermes",
- "newArchEnabled": true,
"assetBundlePatterns": ["**/*"],
"ios": {
"requireFullScreen": true,
@@ -23,7 +21,8 @@
},
"UISupportsTrueScreenSizeOnMac": true,
"UIFileSharingEnabled": true,
- "LSSupportsOpeningDocumentsInPlace": true
+ "LSSupportsOpeningDocumentsInPlace": true,
+ "AVInitialRouteSharingPolicy": "LongFormAudio"
},
"config": {
"usesNonExemptEncryption": false
@@ -37,8 +36,7 @@
"appleTeamId": "MWD5K362T8"
},
"android": {
- "jsEngine": "hermes",
- "versionCode": 92,
+ "versionCode": 93,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png",
@@ -55,23 +53,41 @@
"googleServicesFile": "./google-services.json"
},
"plugins": [
- "@react-native-tvos/config-tv",
+ [
+ "@react-native-tvos/config-tv",
+ {
+ "appleTVImages": {
+ "icon": "./assets/images/icon-tvos.png",
+ "iconSmall": "./assets/images/icon-tvos-small.png",
+ "iconSmall2x": "./assets/images/icon-tvos-small-2x.png",
+ "topShelf": "./assets/images/icon-tvos-topshelf.png",
+ "topShelf2x": "./assets/images/icon-tvos-topshelf-2x.png",
+ "topShelfWide": "./assets/images/icon-tvos-topshelf-wide.png",
+ "topShelfWide2x": "./assets/images/icon-tvos-topshelf-wide-2x.png"
+ },
+ "infoPlist": {
+ "UIAppSupportsHDR": true
+ }
+ }
+ ],
"expo-router",
"expo-font",
"./plugins/withExcludeMedia3Dash.js",
+ "./plugins/withTVUserManagement.js",
[
"expo-build-properties",
{
"ios": {
- "deploymentTarget": "15.6",
- "useFrameworks": "static"
+ "deploymentTarget": "16.4",
+ "useFrameworks": "static",
+ "forceStaticLinking": ["ExpoUI", "GlassEffectView", "GlassPoster"]
},
"android": {
- "buildArchs": ["arm64-v8a", "x86_64"],
+ "buildArchs": ["arm64-v8a", "x86_64", "armeabi-v7a"],
"compileSdkVersion": 36,
"targetSdkVersion": 35,
"buildToolsVersion": "35.0.0",
- "kotlinVersion": "2.0.21",
+ "kotlinVersion": "2.1.20",
"minSdkVersion": 26,
"usesCleartextTraffic": true,
"packagingOptions": {
@@ -118,14 +134,18 @@
"expo-web-browser",
["./plugins/with-runtime-framework-headers.js"],
["./plugins/withChangeNativeAndroidTextToWhite.js"],
+ ["./plugins/withAndroidAlertColors.js"],
["./plugins/withAndroidManifest.js"],
["./plugins/withTrustLocalCerts.js"],
["./plugins/withGradleProperties.js"],
+ ["./plugins/withTVOSAppIcon.js"],
+ ["./plugins/withTVOSTopShelf.js"],
+ ["./plugins/withTVXcodeEnv.js"],
[
"./plugins/withGitPod.js",
{
- "podName": "MPVKit-GPL",
- "podspecUrl": "https://raw.githubusercontent.com/streamyfin/MPVKit/0.40.0-av/MPVKit-GPL.podspec"
+ "podName": "MPVKit",
+ "podspecUrl": "https://raw.githubusercontent.com/mpv-ios/MPVKit/0.41.0-av/MPVKit.podspec"
}
]
],
diff --git a/app/(auth)/(tabs)/(custom-links)/_layout.tsx b/app/(auth)/(tabs)/(custom-links)/_layout.tsx
index 3b8a58e20..67648e05a 100644
--- a/app/(auth)/(tabs)/(custom-links)/_layout.tsx
+++ b/app/(auth)/(tabs)/(custom-links)/_layout.tsx
@@ -9,7 +9,7 @@ export default function CustomMenuLayout() {
([]);
diff --git a/app/(auth)/(tabs)/(favorites)/index.tsx b/app/(auth)/(tabs)/(favorites)/index.tsx
index 198695a8c..10fffe9d0 100644
--- a/app/(auth)/(tabs)/(favorites)/index.tsx
+++ b/app/(auth)/(tabs)/(favorites)/index.tsx
@@ -2,9 +2,10 @@ import { useCallback, useState } from "react";
import { Platform, RefreshControl, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Favorites } from "@/components/home/Favorites";
+import { Favorites as TVFavorites } from "@/components/home/Favorites.tv";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
-export default function favorites() {
+export default function FavoritesPage() {
const invalidateCache = useInvalidatePlaybackProgressCache();
const [loading, setLoading] = useState(false);
@@ -15,6 +16,10 @@ export default function favorites() {
}, []);
const insets = useSafeAreaInsets();
+ if (Platform.isTV) {
+ return ;
+ }
+
return (
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
+ ,
+ }}
+ />
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
(
- _router.back()}
- className='pl-0.5'
- style={{ marginRight: Platform.OS === "android" ? 16 : 0 }}
- >
-
-
- ),
+ headerLeft: () => ,
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
@@ -336,12 +250,8 @@ export default function IndexLayout() {
name='collections/[collectionId]'
options={{
title: "",
- headerLeft: () => (
- _router.back()} className='pl-0.5'>
-
-
- ),
- headerShown: true,
+ headerLeft: () => ,
+ headerShown: !Platform.isTV,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
diff --git a/app/(auth)/(tabs)/(home)/companion-login.tsx b/app/(auth)/(tabs)/(home)/companion-login.tsx
new file mode 100644
index 000000000..68dd2254c
--- /dev/null
+++ b/app/(auth)/(tabs)/(home)/companion-login.tsx
@@ -0,0 +1,7 @@
+import { Platform } from "react-native";
+import { CompanionLoginScreen } from "@/components/companion/CompanionLoginScreen";
+
+export default function CompanionLoginPage() {
+ if (Platform.isTV) return null;
+ return ;
+}
diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx
index fb8ef0b9e..884b1fbb2 100644
--- a/app/(auth)/(tabs)/(home)/downloads/index.tsx
+++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx
@@ -20,7 +20,7 @@ import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
import { queueAtom } from "@/utils/atoms/queue";
import { writeToLog } from "@/utils/log";
-export default function page() {
+export default function DownloadsPage() {
const navigation = useNavigation();
const { t } = useTranslation();
const [_queue, _setQueue] = useAtom(queueAtom);
diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx
index ad951c36f..9586d465d 100644
--- a/app/(auth)/(tabs)/(home)/index.tsx
+++ b/app/(auth)/(tabs)/(home)/index.tsx
@@ -1,15 +1,6 @@
-import { useSettings } from "@/utils/atoms/settings";
import { Home } from "../../../../components/home/Home";
-import { HomeWithCarousel } from "../../../../components/home/HomeWithCarousel";
const Index = () => {
- const { settings } = useSettings();
- const showLargeHomeCarousel = settings.showLargeHomeCarousel ?? false;
-
- if (showLargeHomeCarousel) {
- return ;
- }
-
return ;
};
diff --git a/app/(auth)/(tabs)/(home)/sessions/index.tsx b/app/(auth)/(tabs)/(home)/sessions/index.tsx
index 0ed8fc940..d8a3590d5 100644
--- a/app/(auth)/(tabs)/(home)/sessions/index.tsx
+++ b/app/(auth)/(tabs)/(home)/sessions/index.tsx
@@ -23,7 +23,7 @@ import { formatBitrate } from "@/utils/bitrate";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { formatTimeString } from "@/utils/time";
-export default function page() {
+export default function SessionsPage() {
const { sessions, isLoading } = useSessions({} as useSessionsProps);
const { t } = useTranslation();
@@ -72,7 +72,7 @@ const SessionCard = ({ session }: SessionCardProps) => {
};
const getProgressPercentage = () => {
- if (!session.NowPlayingItem || !session.NowPlayingItem.RunTimeTicks) {
+ if (!session.NowPlayingItem?.RunTimeTicks) {
return 0;
}
diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx
index 76675ae84..db223b2bf 100644
--- a/app/(auth)/(tabs)/(home)/settings.tsx
+++ b/app/(auth)/(tabs)/(home)/settings.tsx
@@ -14,7 +14,11 @@ import { UserInfo } from "@/components/settings/UserInfo";
import useRouter from "@/hooks/useAppRouter";
import { useJellyfin, userAtom } from "@/providers/JellyfinProvider";
-export default function settings() {
+// TV-specific settings component
+const SettingsTV = Platform.isTV ? require("./settings.tv").default : null;
+
+// Mobile settings component
+function SettingsMobile() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [_user] = useAtom(userAtom);
@@ -55,6 +59,18 @@ export default function settings() {
+
+
+
+ router.push("/(auth)/(tabs)/(home)/companion-login")
+ }
+ title={t("pairing.pair_with_phone")}
+ textColor='blue'
+ />
+
+
+
@@ -104,8 +120,17 @@ export default function settings() {
- {!Platform.isTV && }
+
);
}
+
+export default function settings() {
+ // Use TV settings component on TV platforms
+ if (Platform.isTV && SettingsTV) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx
new file mode 100644
index 000000000..8fb8dcef5
--- /dev/null
+++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx
@@ -0,0 +1,949 @@
+import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
+import { useQueryClient } from "@tanstack/react-query";
+import { Directory, Paths } from "expo-file-system";
+import { Image } from "expo-image";
+import { useAtom } from "jotai";
+import { useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Alert, ScrollView, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { Text } from "@/components/common/Text";
+import { TVPasswordEntryModal } from "@/components/login/TVPasswordEntryModal";
+import { TVPINEntryModal } from "@/components/login/TVPINEntryModal";
+import type { TVOptionItem } from "@/components/tv";
+import {
+ TVLogoutButton,
+ TVSectionHeader,
+ TVSettingsOptionButton,
+ TVSettingsRow,
+ TVSettingsStepper,
+ TVSettingsTextInput,
+ TVSettingsToggle,
+} from "@/components/tv";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { useTVOptionModal } from "@/hooks/useTVOptionModal";
+import { useTVUserSwitchModal } from "@/hooks/useTVUserSwitchModal";
+import { APP_LANGUAGES } from "@/i18n";
+import { clearCache as clearAudioCache } from "@/providers/AudioStorage";
+import {
+ apiAtom,
+ cacheVersionAtom,
+ useJellyfin,
+ userAtom,
+} from "@/providers/JellyfinProvider";
+import {
+ AudioTranscodeMode,
+ InactivityTimeout,
+ type MpvCacheMode,
+ type MpvVoDriver,
+ TVTypographyScale,
+ useSettings,
+} from "@/utils/atoms/settings";
+import { storage } from "@/utils/mmkv";
+import {
+ getPreviousServers,
+ type SavedServer,
+ type SavedServerAccount,
+} from "@/utils/secureCredentials";
+import { clearTopShelfCacheSafely } from "@/utils/topshelf/cache";
+
+export default function SettingsTV() {
+ const { t } = useTranslation();
+ const insets = useSafeAreaInsets();
+ const { settings, updateSettings } = useSettings();
+ const { logout, loginWithSavedCredential, loginWithPassword } = useJellyfin();
+ const [user] = useAtom(userAtom);
+ const [api] = useAtom(apiAtom);
+ const [, setCacheVersion] = useAtom(cacheVersionAtom);
+ const { showOptions } = useTVOptionModal();
+ const { showUserSwitchModal } = useTVUserSwitchModal();
+ const typography = useScaledTVTypography();
+ const queryClient = useQueryClient();
+
+ // Local state for OpenSubtitles API key (only commit on blur)
+ const [openSubtitlesApiKey, setOpenSubtitlesApiKey] = useState(
+ settings.openSubtitlesApiKey || "",
+ );
+
+ // PIN/Password modal state for user switching
+ const [pinModalVisible, setPinModalVisible] = useState(false);
+ const [passwordModalVisible, setPasswordModalVisible] = useState(false);
+ const [selectedServer, setSelectedServer] = useState(
+ null,
+ );
+ const [selectedAccount, setSelectedAccount] =
+ useState(null);
+
+ // Track if any modal is open to disable background focus
+ const isAnyModalOpen = pinModalVisible || passwordModalVisible;
+
+ // Get current server and other accounts
+ const currentServer = useMemo(() => {
+ if (!api?.basePath) return null;
+ const servers = getPreviousServers();
+ return servers.find((s) => s.address === api.basePath) || null;
+ }, [api?.basePath]);
+
+ const otherAccounts = useMemo(() => {
+ if (!currentServer || !user?.Id) return [];
+ return currentServer.accounts.filter(
+ (account) => account.userId !== user.Id,
+ );
+ }, [currentServer, user?.Id]);
+
+ const hasOtherAccounts = otherAccounts.length > 0;
+
+ // Handle account selection from modal
+ const handleAccountSelect = async (account: SavedServerAccount) => {
+ if (!currentServer) return;
+
+ if (account.securityType === "none") {
+ // Direct login with saved credential
+ try {
+ await loginWithSavedCredential(currentServer.address, account.userId);
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : t("server.session_expired");
+ const isSessionExpired = errorMessage.includes(
+ t("server.session_expired"),
+ );
+ Alert.alert(
+ isSessionExpired
+ ? t("server.session_expired")
+ : t("login.connection_failed"),
+ isSessionExpired ? t("server.please_login_again") : errorMessage,
+ );
+ }
+ } else if (account.securityType === "pin") {
+ // Show PIN modal
+ setSelectedServer(currentServer);
+ setSelectedAccount(account);
+ setPinModalVisible(true);
+ } else if (account.securityType === "password") {
+ // Show password modal
+ setSelectedServer(currentServer);
+ setSelectedAccount(account);
+ setPasswordModalVisible(true);
+ }
+ };
+
+ // Handle successful PIN entry
+ const handlePinSuccess = async () => {
+ setPinModalVisible(false);
+ if (selectedServer && selectedAccount) {
+ try {
+ await loginWithSavedCredential(
+ selectedServer.address,
+ selectedAccount.userId,
+ );
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : t("server.session_expired");
+ const isSessionExpired = errorMessage.includes(
+ t("server.session_expired"),
+ );
+ Alert.alert(
+ isSessionExpired
+ ? t("server.session_expired")
+ : t("login.connection_failed"),
+ isSessionExpired ? t("server.please_login_again") : errorMessage,
+ );
+ }
+ }
+ setSelectedServer(null);
+ setSelectedAccount(null);
+ };
+
+ // Handle password submission
+ const handlePasswordSubmit = async (password: string) => {
+ if (selectedServer && selectedAccount) {
+ await loginWithPassword(
+ selectedServer.address,
+ selectedAccount.username,
+ password,
+ );
+ }
+ setPasswordModalVisible(false);
+ setSelectedServer(null);
+ setSelectedAccount(null);
+ };
+
+ // Handle switch user button press
+ const handleSwitchUser = () => {
+ if (!currentServer || !user?.Id) return;
+ showUserSwitchModal(currentServer, user.Id, {
+ onAccountSelect: handleAccountSelect,
+ });
+ };
+
+ // Handle clearing all cache in the entire app
+ const handleClearCache = async () => {
+ Alert.alert(
+ t("home.settings.storage.clear_all_cache_confirm", "Clear All Cache?"),
+ t(
+ "home.settings.storage.clear_all_cache_confirm_desc",
+ "Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
+ ),
+ [
+ {
+ text: t("common.cancel", "Cancel"),
+ style: "cancel",
+ },
+ {
+ text: t("common.ok", "OK"),
+ onPress: async () => {
+ try {
+ // 1. Clear React Query Cache (memory & MMKV)
+ storage.remove("REACT_QUERY_OFFLINE_CACHE");
+ await queryClient.resetQueries();
+
+ // 2. Clear expo-image cache (memory & disk)
+ await Image.clearDiskCache();
+ Image.clearMemoryCache();
+
+ // 3. Clear AudioStorage (music) cache
+ await clearAudioCache();
+
+ // 4. Clear TopShelf cache
+ clearTopShelfCacheSafely();
+
+ // 5. Clear Subtitle Cache
+ storage.remove("downloadedSubtitles.json");
+ const subtitlesDir = new Directory(
+ Paths.cache,
+ "streamyfin-subtitles",
+ );
+ if (subtitlesDir.exists) {
+ await subtitlesDir.delete();
+ }
+
+ // 6. Clear MMKV caches like extracted image colors and other non-essential storage keys
+ const keysToKeep = [
+ "settings",
+ "serverUrl",
+ "token",
+ "user",
+ "deviceId",
+ "previousServers",
+ "hasAskedForNotificationPermission",
+ "hasShownIntro",
+ "multiAccountMigrated",
+ "selectedTVServer",
+ "downloads.v2.json",
+ ];
+ const allKeys = storage.getAllKeys();
+ for (const key of allKeys) {
+ if (!keysToKeep.includes(key)) {
+ storage.remove(key);
+ }
+ }
+
+ // 7. Increment cache version to force remount of components
+ setCacheVersion((v) => v + 1);
+ } catch (error) {
+ console.error("Failed to clear cache:", error);
+ Alert.alert(
+ t("home.settings.toasts.error_deleting_files", "Error"),
+ t(
+ "home.settings.storage.clear_all_cache_error_desc",
+ "An error occurred while clearing the cache.",
+ ),
+ );
+ }
+ },
+ },
+ ],
+ );
+ };
+
+ const currentAudioTranscode =
+ settings.audioTranscodeMode || AudioTranscodeMode.Auto;
+ const currentSubtitleMode =
+ settings.subtitleMode || SubtitlePlaybackMode.Default;
+ const currentAlignX = settings.mpvSubtitleAlignX ?? "center";
+ const currentAlignY = settings.mpvSubtitleAlignY ?? "bottom";
+ const currentTypographyScale =
+ settings.tvTypographyScale || TVTypographyScale.Default;
+ const currentCacheMode = settings.mpvCacheEnabled ?? "auto";
+ const currentVoDriver = settings.mpvVoDriver ?? "gpu-next";
+ const currentLanguage = settings.preferedLanguage;
+
+ // Audio transcoding options
+ const audioTranscodeModeOptions: TVOptionItem[] = useMemo(
+ () => [
+ {
+ label: t("home.settings.audio.transcode_mode.auto"),
+ value: AudioTranscodeMode.Auto,
+ selected: currentAudioTranscode === AudioTranscodeMode.Auto,
+ },
+ {
+ label: t("home.settings.audio.transcode_mode.stereo"),
+ value: AudioTranscodeMode.ForceStereo,
+ selected: currentAudioTranscode === AudioTranscodeMode.ForceStereo,
+ },
+ {
+ label: t("home.settings.audio.transcode_mode.5_1"),
+ value: AudioTranscodeMode.Allow51,
+ selected: currentAudioTranscode === AudioTranscodeMode.Allow51,
+ },
+ {
+ label: t("home.settings.audio.transcode_mode.passthrough"),
+ value: AudioTranscodeMode.AllowAll,
+ selected: currentAudioTranscode === AudioTranscodeMode.AllowAll,
+ },
+ ],
+ [t, currentAudioTranscode],
+ );
+
+ // Subtitle mode options
+ const subtitleModeOptions: TVOptionItem[] = useMemo(
+ () => [
+ {
+ label: t("home.settings.subtitles.modes.Default"),
+ value: SubtitlePlaybackMode.Default,
+ selected: currentSubtitleMode === SubtitlePlaybackMode.Default,
+ },
+ {
+ label: t("home.settings.subtitles.modes.Smart"),
+ value: SubtitlePlaybackMode.Smart,
+ selected: currentSubtitleMode === SubtitlePlaybackMode.Smart,
+ },
+ {
+ label: t("home.settings.subtitles.modes.OnlyForced"),
+ value: SubtitlePlaybackMode.OnlyForced,
+ selected: currentSubtitleMode === SubtitlePlaybackMode.OnlyForced,
+ },
+ {
+ label: t("home.settings.subtitles.modes.Always"),
+ value: SubtitlePlaybackMode.Always,
+ selected: currentSubtitleMode === SubtitlePlaybackMode.Always,
+ },
+ {
+ label: t("home.settings.subtitles.modes.None"),
+ value: SubtitlePlaybackMode.None,
+ selected: currentSubtitleMode === SubtitlePlaybackMode.None,
+ },
+ ],
+ [t, currentSubtitleMode],
+ );
+
+ // MPV alignment options
+ const alignXOptions: TVOptionItem[] = useMemo(
+ () => [
+ { label: "Left", value: "left", selected: currentAlignX === "left" },
+ {
+ label: "Center",
+ value: "center",
+ selected: currentAlignX === "center",
+ },
+ { label: "Right", value: "right", selected: currentAlignX === "right" },
+ ],
+ [currentAlignX],
+ );
+
+ const alignYOptions: TVOptionItem[] = useMemo(
+ () => [
+ { label: "Top", value: "top", selected: currentAlignY === "top" },
+ {
+ label: "Center",
+ value: "center",
+ selected: currentAlignY === "center",
+ },
+ {
+ label: "Bottom",
+ value: "bottom",
+ selected: currentAlignY === "bottom",
+ },
+ ],
+ [currentAlignY],
+ );
+
+ // Cache mode options
+ const cacheModeOptions: TVOptionItem[] = useMemo(
+ () => [
+ {
+ label: t("home.settings.buffer.cache_auto"),
+ value: "auto",
+ selected: currentCacheMode === "auto",
+ },
+ {
+ label: t("home.settings.buffer.cache_yes"),
+ value: "yes",
+ selected: currentCacheMode === "yes",
+ },
+ {
+ label: t("home.settings.buffer.cache_no"),
+ value: "no",
+ selected: currentCacheMode === "no",
+ },
+ ],
+ [t, currentCacheMode],
+ );
+
+ // VO driver options
+ const voDriverOptions: TVOptionItem[] = useMemo(
+ () => [
+ {
+ label: t("home.settings.vo_driver.gpu_next"),
+ value: "gpu-next",
+ selected: currentVoDriver === "gpu-next",
+ },
+ {
+ label: t("home.settings.vo_driver.gpu"),
+ value: "gpu",
+ selected: currentVoDriver === "gpu",
+ },
+ ],
+ [t, currentVoDriver],
+ );
+
+ // Typography scale options
+ const typographyScaleOptions: TVOptionItem[] = useMemo(
+ () => [
+ {
+ label: t("home.settings.appearance.display_size_small"),
+ value: TVTypographyScale.Small,
+ selected: currentTypographyScale === TVTypographyScale.Small,
+ },
+ {
+ label: t("home.settings.appearance.display_size_default"),
+ value: TVTypographyScale.Default,
+ selected: currentTypographyScale === TVTypographyScale.Default,
+ },
+ {
+ label: t("home.settings.appearance.display_size_large"),
+ value: TVTypographyScale.Large,
+ selected: currentTypographyScale === TVTypographyScale.Large,
+ },
+ {
+ label: t("home.settings.appearance.display_size_extra_large"),
+ value: TVTypographyScale.ExtraLarge,
+ selected: currentTypographyScale === TVTypographyScale.ExtraLarge,
+ },
+ ],
+ [t, currentTypographyScale],
+ );
+
+ // Language options
+ const languageOptions: TVOptionItem[] = useMemo(
+ () => [
+ {
+ label: t("home.settings.languages.system"),
+ value: undefined,
+ selected: !currentLanguage,
+ },
+ ...APP_LANGUAGES.map((lang) => ({
+ label: lang.label,
+ value: lang.value,
+ selected: currentLanguage === lang.value,
+ })),
+ ],
+ [t, currentLanguage],
+ );
+
+ // Inactivity timeout options (TV security feature)
+ const currentInactivityTimeout =
+ settings.inactivityTimeout ?? InactivityTimeout.Disabled;
+
+ const inactivityTimeoutOptions: TVOptionItem[] = useMemo(
+ () => [
+ {
+ label: t("home.settings.security.inactivity_timeout.disabled"),
+ value: InactivityTimeout.Disabled,
+ selected: currentInactivityTimeout === InactivityTimeout.Disabled,
+ },
+ {
+ label: t("home.settings.security.inactivity_timeout.1_minute"),
+ value: InactivityTimeout.OneMinute,
+ selected: currentInactivityTimeout === InactivityTimeout.OneMinute,
+ },
+ {
+ label: t("home.settings.security.inactivity_timeout.5_minutes"),
+ value: InactivityTimeout.FiveMinutes,
+ selected: currentInactivityTimeout === InactivityTimeout.FiveMinutes,
+ },
+ {
+ label: t("home.settings.security.inactivity_timeout.15_minutes"),
+ value: InactivityTimeout.FifteenMinutes,
+ selected: currentInactivityTimeout === InactivityTimeout.FifteenMinutes,
+ },
+ {
+ label: t("home.settings.security.inactivity_timeout.30_minutes"),
+ value: InactivityTimeout.ThirtyMinutes,
+ selected: currentInactivityTimeout === InactivityTimeout.ThirtyMinutes,
+ },
+ {
+ label: t("home.settings.security.inactivity_timeout.1_hour"),
+ value: InactivityTimeout.OneHour,
+ selected: currentInactivityTimeout === InactivityTimeout.OneHour,
+ },
+ {
+ label: t("home.settings.security.inactivity_timeout.4_hours"),
+ value: InactivityTimeout.FourHours,
+ selected: currentInactivityTimeout === InactivityTimeout.FourHours,
+ },
+ {
+ label: t("home.settings.security.inactivity_timeout.24_hours"),
+ value: InactivityTimeout.TwentyFourHours,
+ selected:
+ currentInactivityTimeout === InactivityTimeout.TwentyFourHours,
+ },
+ ],
+ [t, currentInactivityTimeout],
+ );
+
+ // Get display labels for option buttons
+ const audioTranscodeLabel = useMemo(() => {
+ const option = audioTranscodeModeOptions.find((o) => o.selected);
+ return option?.label || t("home.settings.audio.transcode_mode.auto");
+ }, [audioTranscodeModeOptions, t]);
+
+ const subtitleModeLabel = useMemo(() => {
+ const option = subtitleModeOptions.find((o) => o.selected);
+ return option?.label || t("home.settings.subtitles.modes.Default");
+ }, [subtitleModeOptions, t]);
+
+ const alignXLabel = useMemo(() => {
+ const option = alignXOptions.find((o) => o.selected);
+ return option?.label || "Center";
+ }, [alignXOptions]);
+
+ const alignYLabel = useMemo(() => {
+ const option = alignYOptions.find((o) => o.selected);
+ return option?.label || "Bottom";
+ }, [alignYOptions]);
+
+ const typographyScaleLabel = useMemo(() => {
+ const option = typographyScaleOptions.find((o) => o.selected);
+ return option?.label || t("home.settings.appearance.display_size_default");
+ }, [typographyScaleOptions, t]);
+
+ const cacheModeLabel = useMemo(() => {
+ const option = cacheModeOptions.find((o) => o.selected);
+ return option?.label || t("home.settings.buffer.cache_auto");
+ }, [cacheModeOptions, t]);
+
+ const voDriverLabel = useMemo(() => {
+ const option = voDriverOptions.find((o) => o.selected);
+ return option?.label || t("home.settings.vo_driver.gpu_next");
+ }, [voDriverOptions, t]);
+
+ const languageLabel = useMemo(() => {
+ if (!currentLanguage) return t("home.settings.languages.system");
+ const option = APP_LANGUAGES.find((l) => l.value === currentLanguage);
+ return option?.label || t("home.settings.languages.system");
+ }, [currentLanguage, t]);
+
+ const inactivityTimeoutLabel = useMemo(() => {
+ const option = inactivityTimeoutOptions.find((o) => o.selected);
+ return (
+ option?.label || t("home.settings.security.inactivity_timeout.disabled")
+ );
+ }, [inactivityTimeoutOptions, t]);
+
+ return (
+
+
+
+ {/* Header */}
+
+ {t("home.settings.settings_title")}
+
+
+ {/* Account Section */}
+
+
+
+ {/* Security Section */}
+
+
+ showOptions({
+ title: t("home.settings.security.inactivity_timeout.title"),
+ options: inactivityTimeoutOptions,
+ onSelect: (value) =>
+ updateSettings({ inactivityTimeout: value }),
+ })
+ }
+ />
+
+ {/* Audio Section */}
+
+
+ showOptions({
+ title: t("home.settings.audio.transcode_mode.title"),
+ options: audioTranscodeModeOptions,
+ onSelect: (value) =>
+ updateSettings({ audioTranscodeMode: value }),
+ })
+ }
+ />
+
+ {/* Subtitles Section */}
+
+
+ showOptions({
+ title: t("home.settings.subtitles.subtitle_mode"),
+ options: subtitleModeOptions,
+ onSelect: (value) => updateSettings({ subtitleMode: value }),
+ })
+ }
+ />
+
+ updateSettings({ rememberSubtitleSelections: value })
+ }
+ />
+ {
+ const newValue = Math.max(
+ 0.1,
+ (settings.mpvSubtitleScale ?? 1.0) - 0.1,
+ );
+ updateSettings({
+ mpvSubtitleScale: Math.round(newValue * 10) / 10,
+ });
+ }}
+ onIncrease={() => {
+ const newValue = Math.min(
+ 3.0,
+ (settings.mpvSubtitleScale ?? 1.0) + 0.1,
+ );
+ updateSettings({
+ mpvSubtitleScale: Math.round(newValue * 10) / 10,
+ });
+ }}
+ formatValue={(v) => `${v.toFixed(1)}x`}
+ />
+ {
+ const newValue = Math.max(
+ 0,
+ (settings.mpvSubtitleMarginY ?? 0) - 5,
+ );
+ updateSettings({ mpvSubtitleMarginY: newValue });
+ }}
+ onIncrease={() => {
+ const newValue = Math.min(
+ 100,
+ (settings.mpvSubtitleMarginY ?? 0) + 5,
+ );
+ updateSettings({ mpvSubtitleMarginY: newValue });
+ }}
+ />
+
+ showOptions({
+ title: "Horizontal Alignment",
+ options: alignXOptions,
+ onSelect: (value) =>
+ updateSettings({
+ mpvSubtitleAlignX: value as "left" | "center" | "right",
+ }),
+ })
+ }
+ />
+
+ showOptions({
+ title: "Vertical Alignment",
+ options: alignYOptions,
+ onSelect: (value) =>
+ updateSettings({
+ mpvSubtitleAlignY: value as "top" | "center" | "bottom",
+ }),
+ })
+ }
+ />
+
+ {/* OpenSubtitles Section */}
+
+
+ {t("home.settings.subtitles.opensubtitles_hint") ||
+ "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured."}
+
+ updateSettings({ openSubtitlesApiKey })}
+ secureTextEntry
+ />
+
+ {t("home.settings.subtitles.opensubtitles_get_key") ||
+ "Get your free API key at opensubtitles.com/en/consumers"}
+
+
+ {/* Buffer Settings Section */}
+
+
+ showOptions({
+ title: t("home.settings.buffer.cache_mode"),
+ options: cacheModeOptions,
+ onSelect: (value) => updateSettings({ mpvCacheEnabled: value }),
+ })
+ }
+ />
+
+ {/* Video Output Section */}
+
+
+ showOptions({
+ title: t("home.settings.vo_driver.vo_mode"),
+ options: voDriverOptions,
+ onSelect: (value) => updateSettings({ mpvVoDriver: value }),
+ })
+ }
+ />
+ {
+ const newValue = Math.max(
+ 5,
+ (settings.mpvCacheSeconds ?? 10) - 5,
+ );
+ updateSettings({ mpvCacheSeconds: newValue });
+ }}
+ onIncrease={() => {
+ const newValue = Math.min(
+ 120,
+ (settings.mpvCacheSeconds ?? 10) + 5,
+ );
+ updateSettings({ mpvCacheSeconds: newValue });
+ }}
+ formatValue={(v) => `${v}s`}
+ />
+ {
+ const newValue = Math.max(
+ 50,
+ (settings.mpvDemuxerMaxBytes ?? 150) - 25,
+ );
+ updateSettings({ mpvDemuxerMaxBytes: newValue });
+ }}
+ onIncrease={() => {
+ const newValue = Math.min(
+ 500,
+ (settings.mpvDemuxerMaxBytes ?? 150) + 25,
+ );
+ updateSettings({ mpvDemuxerMaxBytes: newValue });
+ }}
+ formatValue={(v) => `${v} MB`}
+ />
+ {
+ const newValue = Math.max(
+ 25,
+ (settings.mpvDemuxerMaxBackBytes ?? 50) - 25,
+ );
+ updateSettings({ mpvDemuxerMaxBackBytes: newValue });
+ }}
+ onIncrease={() => {
+ const newValue = Math.min(
+ 200,
+ (settings.mpvDemuxerMaxBackBytes ?? 50) + 25,
+ );
+ updateSettings({ mpvDemuxerMaxBackBytes: newValue });
+ }}
+ formatValue={(v) => `${v} MB`}
+ />
+
+ {/* Appearance Section */}
+
+
+ showOptions({
+ title: t("home.settings.appearance.display_size"),
+ options: typographyScaleOptions,
+ onSelect: (value) =>
+ updateSettings({ tvTypographyScale: value }),
+ })
+ }
+ />
+
+ showOptions({
+ title: t("home.settings.languages.app_language"),
+ options: languageOptions,
+ onSelect: (value) =>
+ updateSettings({ preferedLanguage: value }),
+ })
+ }
+ />
+
+ updateSettings({ mergeNextUpAndContinueWatching: value })
+ }
+ />
+ updateSettings({ showHomeBackdrop: value })}
+ />
+ updateSettings({ showTVHeroCarousel: value })}
+ />
+
+ updateSettings({ showSeriesPosterOnEpisode: value })
+ }
+ />
+ updateSettings({ tvThemeMusicEnabled: value })}
+ />
+
+ {/* Storage Section */}
+
+
+
+ {/* User Section */}
+
+
+
+
+ {/* Logout Button */}
+
+
+
+
+
+
+ {/* PIN Entry Modal */}
+ {
+ setPinModalVisible(false);
+ setSelectedAccount(null);
+ setSelectedServer(null);
+ }}
+ onSuccess={handlePinSuccess}
+ onForgotPIN={() => {
+ setPinModalVisible(false);
+ setSelectedAccount(null);
+ setSelectedServer(null);
+ }}
+ serverUrl={selectedServer?.address || ""}
+ userId={selectedAccount?.userId || ""}
+ username={selectedAccount?.username || ""}
+ />
+
+ {/* Password Entry Modal */}
+ {
+ setPasswordModalVisible(false);
+ setSelectedAccount(null);
+ setSelectedServer(null);
+ }}
+ onSubmit={handlePasswordSubmit}
+ username={selectedAccount?.username || ""}
+ />
+
+ );
+}
diff --git a/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx
index a0b3bab9b..24a3011e3 100644
--- a/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx
@@ -12,7 +12,7 @@ import DisabledSetting from "@/components/settings/DisabledSetting";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
-export default function page() {
+export default function AppearanceHideLibrariesPage() {
const { settings, updateSettings, pluginSettings } = useSettings();
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx
index e1c8b56b6..e7a61bde3 100644
--- a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx
@@ -11,7 +11,7 @@ import DisabledSetting from "@/components/settings/DisabledSetting";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
-export default function page() {
+export default function HideLibrariesPage() {
const { settings, updateSettings, pluginSettings } = useSettings();
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
diff --git a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx
index 878319256..4ce5736cd 100644
--- a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx
@@ -61,7 +61,10 @@ export default function Page() {
setLoading(true);
try {
logsFile.write(JSON.stringify(filteredLogs));
- await Sharing.shareAsync(logsFile.uri, { mimeType: "txt", UTI: "txt" });
+ await Sharing.shareAsync(logsFile.uri, {
+ mimeType: "text/plain",
+ UTI: "public.plain-text",
+ });
} catch (e: any) {
writeErrorLog("Something went wrong attempting to export", e);
} finally {
@@ -85,12 +88,7 @@ export default function Page() {
}, [share, loading]);
return (
-
+
-
+
{filteredLogs?.map((log, index) => (
diff --git a/app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx b/app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx
index 370247c15..2771117f9 100644
--- a/app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/playback-controls/page.tsx
@@ -3,6 +3,8 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { GestureControls } from "@/components/settings/GestureControls";
import { MediaProvider } from "@/components/settings/MediaContext";
import { MediaToggles } from "@/components/settings/MediaToggles";
+import { MpvBufferSettings } from "@/components/settings/MpvBufferSettings";
+import { MpvVoSettings } from "@/components/settings/MpvVoSettings";
import { PlaybackControlsSettings } from "@/components/settings/PlaybackControlsSettings";
import { ChromecastSettings } from "../../../../../../components/settings/ChromecastSettings";
@@ -26,6 +28,8 @@ export default function PlaybackControlsPage() {
+
+
{!Platform.isTV && }
diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx
index cd1efca54..84041fd01 100644
--- a/app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/plugins/jellyseerr/page.tsx
@@ -4,7 +4,7 @@ import DisabledSetting from "@/components/settings/DisabledSetting";
import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
import { useSettings } from "@/utils/atoms/settings";
-export default function page() {
+export default function JellyseerrPluginPage() {
const { pluginSettings } = useSettings();
const insets = useSafeAreaInsets();
@@ -18,7 +18,7 @@ export default function page() {
>
diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx
index 05056826e..e9af145fd 100644
--- a/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/plugins/kefinTweaks/page.tsx
@@ -4,7 +4,7 @@ import DisabledSetting from "@/components/settings/DisabledSetting";
import { KefinTweaksSettings } from "@/components/settings/KefinTweaks";
import { useSettings } from "@/utils/atoms/settings";
-export default function page() {
+export default function KefinTweaksPage() {
const { pluginSettings } = useSettings();
const insets = useSafeAreaInsets();
@@ -18,7 +18,7 @@ export default function page() {
>
diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx
index 10be4af58..3ce2c81c3 100644
--- a/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/plugins/marlin-search/page.tsx
@@ -18,7 +18,7 @@ import DisabledSetting from "@/components/settings/DisabledSetting";
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
import { useSettings } from "@/utils/atoms/settings";
-export default function page() {
+export default function MarlinSearchPage() {
const navigation = useNavigation();
const { t } = useTranslation();
diff --git a/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx b/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx
index 697db6c4e..1c4dcd199 100644
--- a/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx
+++ b/app/(auth)/(tabs)/(home)/settings/plugins/streamystats/page.tsx
@@ -17,7 +17,7 @@ import { ListItem } from "@/components/list/ListItem";
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
import { useSettings } from "@/utils/atoms/settings";
-export default function page() {
+export default function StreamystatsPage() {
const { t } = useTranslation();
const navigation = useNavigation();
const insets = useSafeAreaInsets();
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/collections/[collectionId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/collections/[collectionId].tsx
index 1723fe4b2..5fd125c96 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/collections/[collectionId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/collections/[collectionId].tsx
@@ -15,14 +15,24 @@ import { useAtom } from "jotai";
import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
-import { FlatList, View } from "react-native";
+import { FlatList, Platform, useWindowDimensions, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
-import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
+import {
+ getItemNavigation,
+ TouchableItemRouter,
+} from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemCardText } from "@/components/ItemCardText";
+import { Loader } from "@/components/Loader";
import { ItemPoster } from "@/components/posters/ItemPoster";
+import { TVFilterButton } from "@/components/tv";
+import { TVPosterCard } from "@/components/tv/TVPosterCard";
+import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
+import useRouter from "@/hooks/useAppRouter";
+import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
+import { useTVOptionModal } from "@/hooks/useTVOptionModal";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
@@ -36,19 +46,29 @@ import {
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
+import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
+
+const TV_ITEM_GAP = 16;
+const TV_SCALE_PADDING = 20;
const page: React.FC = () => {
const searchParams = useLocalSearchParams();
const { collectionId } = searchParams as { collectionId: string };
+ const posterSizes = useScaledTVPosterSizes();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const navigation = useNavigation();
+ const router = useRouter();
+ const { showOptions } = useTVOptionModal();
+ const { showItemActions } = useTVItemActionModal();
+ const { width: screenWidth } = useWindowDimensions();
const [orientation, _setOrientation] = useState(
ScreenOrientation.Orientation.PORTRAIT_UP,
);
const { t } = useTranslation();
+ const insets = useSafeAreaInsets();
const [selectedGenres, setSelectedGenres] = useAtom(genreFilterAtom);
const [selectedYears, setSelectedYears] = useAtom(yearFilterAtom);
@@ -56,7 +76,7 @@ const page: React.FC = () => {
const [sortBy, setSortBy] = useAtom(sortByAtom);
const [sortOrder, setSortOrder] = useAtom(sortOrderAtom);
- const { data: collection } = useQuery({
+ const { data: collection, isLoading: isCollectionLoading } = useQuery({
queryKey: ["collection", collectionId],
queryFn: async () => {
if (!api) return null;
@@ -71,6 +91,46 @@ const page: React.FC = () => {
staleTime: 60 * 1000,
});
+ // TV Filter queries
+ const { data: tvGenreOptions } = useQuery({
+ queryKey: ["filters", "Genres", "tvGenreFilter", collectionId],
+ queryFn: async () => {
+ if (!api) return [];
+ const response = await getFilterApi(api).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: collectionId,
+ });
+ return response.data.Genres || [];
+ },
+ enabled: Platform.isTV && !!api && !!user?.Id && !!collectionId,
+ });
+
+ const { data: tvYearOptions } = useQuery({
+ queryKey: ["filters", "Years", "tvYearFilter", collectionId],
+ queryFn: async () => {
+ if (!api) return [];
+ const response = await getFilterApi(api).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: collectionId,
+ });
+ return response.data.Years || [];
+ },
+ enabled: Platform.isTV && !!api && !!user?.Id && !!collectionId,
+ });
+
+ const { data: tvTagOptions } = useQuery({
+ queryKey: ["filters", "Tags", "tvTagFilter", collectionId],
+ queryFn: async () => {
+ if (!api) return [];
+ const response = await getFilterApi(api).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: collectionId,
+ });
+ return response.data.Tags || [];
+ },
+ enabled: Platform.isTV && !!api && !!user?.Id && !!collectionId,
+ });
+
useEffect(() => {
navigation.setOptions({ title: collection?.Name || "" });
setSortOrder([SortOrderOption.Ascending]);
@@ -87,6 +147,18 @@ const page: React.FC = () => {
setSortBy([sortByOption]);
}, [navigation, collection]);
+ // Calculate columns for TV grid
+ const nrOfCols = useMemo(() => {
+ if (Platform.isTV) {
+ const itemWidth = posterSizes.poster + TV_ITEM_GAP;
+ return Math.max(
+ 1,
+ Math.floor((screenWidth - TV_SCALE_PADDING * 2) / itemWidth),
+ );
+ }
+ return orientation === ScreenOrientation.Orientation.PORTRAIT_UP ? 3 : 5;
+ }, [screenWidth, orientation]);
+
const fetchItems = useCallback(
async ({
pageParam,
@@ -98,7 +170,7 @@ const page: React.FC = () => {
const response = await getItemsApi(api).getItems({
userId: user?.Id,
parentId: collectionId,
- limit: 18,
+ limit: Platform.isTV ? 36 : 18,
startIndex: pageParam,
// Set one ordering at a time. As collections do not work with correctly with multiple.
sortBy: [sortBy[0]],
@@ -114,7 +186,7 @@ const page: React.FC = () => {
genres: selectedGenres,
tags: selectedTags,
years: selectedYears.map((year) => Number.parseInt(year, 10)),
- includeItemTypes: ["Movie", "Series"],
+ includeItemTypes: ["Movie", "Series", "Season"],
});
return response.data || null;
@@ -123,6 +195,7 @@ const page: React.FC = () => {
api,
user?.Id,
collection,
+ collectionId,
selectedGenres,
selectedYears,
selectedTags,
@@ -131,39 +204,40 @@ const page: React.FC = () => {
],
);
- const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
- queryKey: [
- "collection-items",
- collection,
- selectedGenres,
- selectedYears,
- selectedTags,
- sortBy,
- sortOrder,
- ],
- queryFn: fetchItems,
- getNextPageParam: (lastPage, pages) => {
- if (
- !lastPage?.Items ||
- !lastPage?.TotalRecordCount ||
- lastPage?.TotalRecordCount === 0
- )
+ const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
+ useInfiniteQuery({
+ queryKey: [
+ "collection-items",
+ collectionId,
+ selectedGenres,
+ selectedYears,
+ selectedTags,
+ sortBy,
+ sortOrder,
+ ],
+ queryFn: fetchItems,
+ getNextPageParam: (lastPage, pages) => {
+ if (
+ !lastPage?.Items ||
+ !lastPage?.TotalRecordCount ||
+ lastPage?.TotalRecordCount === 0
+ )
+ return undefined;
+
+ const totalItems = lastPage.TotalRecordCount;
+ const accumulatedItems = pages.reduce(
+ (acc, curr) => acc + (curr?.Items?.length || 0),
+ 0,
+ );
+
+ if (accumulatedItems < totalItems) {
+ return lastPage?.Items?.length * pages.length;
+ }
return undefined;
-
- const totalItems = lastPage.TotalRecordCount;
- const accumulatedItems = pages.reduce(
- (acc, curr) => acc + (curr?.Items?.length || 0),
- 0,
- );
-
- if (accumulatedItems < totalItems) {
- return lastPage?.Items?.length * pages.length;
- }
- return undefined;
- },
- initialPageParam: 0,
- enabled: !!api && !!user?.Id && !!collection,
- });
+ },
+ initialPageParam: 0,
+ enabled: !!api && !!user?.Id && !!collection,
+ });
const flatData = useMemo(() => {
return (
@@ -195,7 +269,6 @@ const page: React.FC = () => {
}}
>
- {/* */}
@@ -203,9 +276,34 @@ const page: React.FC = () => {
[orientation],
);
- const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []);
+ const renderTVItem = useCallback(
+ ({ item }: { item: BaseItemDto }) => {
+ const handlePress = () => {
+ const navTarget = getItemNavigation(item, "(home)");
+ router.push(navTarget as any);
+ };
- const _insets = useSafeAreaInsets();
+ return (
+
+ showItemActions(item)}
+ width={posterSizes.poster}
+ />
+
+ );
+ },
+ [router, showItemActions, posterSizes.poster],
+ );
+
+ const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []);
const ListHeaderComponent = useCallback(
() => (
@@ -372,48 +470,315 @@ const page: React.FC = () => {
],
);
+ // TV Filter options - with "All" option for clearable filters
+ const tvGenreFilterOptions = useMemo(
+ (): TVOptionItem[] => [
+ {
+ label: t("library.filters.all"),
+ value: "__all__",
+ selected: selectedGenres.length === 0,
+ },
+ ...(tvGenreOptions || []).map((genre) => ({
+ label: genre,
+ value: genre,
+ selected: selectedGenres.includes(genre),
+ })),
+ ],
+ [tvGenreOptions, selectedGenres, t],
+ );
+
+ const tvYearFilterOptions = useMemo(
+ (): TVOptionItem[] => [
+ {
+ label: t("library.filters.all"),
+ value: "__all__",
+ selected: selectedYears.length === 0,
+ },
+ ...(tvYearOptions || []).map((year) => ({
+ label: String(year),
+ value: String(year),
+ selected: selectedYears.includes(String(year)),
+ })),
+ ],
+ [tvYearOptions, selectedYears, t],
+ );
+
+ const tvTagFilterOptions = useMemo(
+ (): TVOptionItem[] => [
+ {
+ label: t("library.filters.all"),
+ value: "__all__",
+ selected: selectedTags.length === 0,
+ },
+ ...(tvTagOptions || []).map((tag) => ({
+ label: tag,
+ value: tag,
+ selected: selectedTags.includes(tag),
+ })),
+ ],
+ [tvTagOptions, selectedTags, t],
+ );
+
+ const tvSortByOptions = useMemo(
+ (): TVOptionItem[] =>
+ sortOptions.map((option) => ({
+ label: option.value,
+ value: option.key,
+ selected: sortBy[0] === option.key,
+ })),
+ [sortBy],
+ );
+
+ const tvSortOrderOptions = useMemo(
+ (): TVOptionItem[] =>
+ sortOrderOptions.map((option) => ({
+ label: option.value,
+ value: option.key,
+ selected: sortOrder[0] === option.key,
+ })),
+ [sortOrder],
+ );
+
+ // TV Filter handlers using navigation-based modal
+ const handleShowGenreFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.genres"),
+ options: tvGenreFilterOptions,
+ onSelect: (value: string) => {
+ if (value === "__all__") {
+ setSelectedGenres([]);
+ } else if (selectedGenres.includes(value)) {
+ setSelectedGenres(selectedGenres.filter((g) => g !== value));
+ } else {
+ setSelectedGenres([...selectedGenres, value]);
+ }
+ },
+ });
+ }, [showOptions, t, tvGenreFilterOptions, selectedGenres, setSelectedGenres]);
+
+ const handleShowYearFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.years"),
+ options: tvYearFilterOptions,
+ onSelect: (value: string) => {
+ if (value === "__all__") {
+ setSelectedYears([]);
+ } else if (selectedYears.includes(value)) {
+ setSelectedYears(selectedYears.filter((y) => y !== value));
+ } else {
+ setSelectedYears([...selectedYears, value]);
+ }
+ },
+ });
+ }, [showOptions, t, tvYearFilterOptions, selectedYears, setSelectedYears]);
+
+ const handleShowTagFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.tags"),
+ options: tvTagFilterOptions,
+ onSelect: (value: string) => {
+ if (value === "__all__") {
+ setSelectedTags([]);
+ } else if (selectedTags.includes(value)) {
+ setSelectedTags(selectedTags.filter((tag) => tag !== value));
+ } else {
+ setSelectedTags([...selectedTags, value]);
+ }
+ },
+ });
+ }, [showOptions, t, tvTagFilterOptions, selectedTags, setSelectedTags]);
+
+ const handleShowSortByFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.sort_by"),
+ options: tvSortByOptions,
+ onSelect: (value: SortByOption) => {
+ setSortBy([value]);
+ },
+ });
+ }, [showOptions, t, tvSortByOptions, setSortBy]);
+
+ const handleShowSortOrderFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.sort_order"),
+ options: tvSortOrderOptions,
+ onSelect: (value: SortOrderOption) => {
+ setSortOrder([value]);
+ },
+ });
+ }, [showOptions, t, tvSortOrderOptions, setSortOrder]);
+
+ // TV filter bar state
+ const hasActiveFilters =
+ selectedGenres.length > 0 ||
+ selectedYears.length > 0 ||
+ selectedTags.length > 0;
+
+ const resetAllFilters = useCallback(() => {
+ setSelectedGenres([]);
+ setSelectedYears([]);
+ setSelectedTags([]);
+ }, [setSelectedGenres, setSelectedYears, setSelectedTags]);
+
+ if (isLoading || isCollectionLoading) {
+ return (
+
+
+
+ );
+ }
+
if (!collection) return null;
- return (
-
-
- {t("search.no_results")}
-
-
- }
- extraData={[
- selectedGenres,
- selectedYears,
- selectedTags,
- sortBy,
- sortOrder,
- ]}
- contentInsetAdjustmentBehavior='automatic'
- data={flatData}
- renderItem={renderItem}
- keyExtractor={keyExtractor}
- numColumns={
- orientation === ScreenOrientation.Orientation.PORTRAIT_UP ? 3 : 5
- }
- onEndReached={() => {
- if (hasNextPage) {
- fetchNextPage();
+ // Mobile return
+ if (!Platform.isTV) {
+ return (
+
+
+ {t("search.no_results")}
+
+
}
- }}
- onEndReachedThreshold={0.5}
- ListHeaderComponent={ListHeaderComponent}
- contentContainerStyle={{ paddingBottom: 24 }}
- ItemSeparatorComponent={() => (
- {
+ if (hasNextPage) {
+ fetchNextPage();
+ }
+ }}
+ onEndReachedThreshold={0.5}
+ ListHeaderComponent={ListHeaderComponent}
+ contentContainerStyle={{ paddingBottom: 24 }}
+ ItemSeparatorComponent={() => (
+
+ )}
+ />
+ );
+ }
+
+ // TV return with filter bar
+ return (
+
+ {/* Filter bar */}
+
+ {hasActiveFilters && (
+
+ )}
+ 0
+ ? `${selectedGenres.length} selected`
+ : t("library.filters.all")
+ }
+ onPress={handleShowGenreFilter}
+ hasTVPreferredFocus={!hasActiveFilters}
+ hasActiveFilter={selectedGenres.length > 0}
/>
- )}
- />
+ 0
+ ? `${selectedYears.length} selected`
+ : t("library.filters.all")
+ }
+ onPress={handleShowYearFilter}
+ hasActiveFilter={selectedYears.length > 0}
+ />
+ 0
+ ? `${selectedTags.length} selected`
+ : t("library.filters.all")
+ }
+ onPress={handleShowTagFilter}
+ hasActiveFilter={selectedTags.length > 0}
+ />
+ o.key === sortBy[0])?.value || ""}
+ onPress={handleShowSortByFilter}
+ />
+ o.key === sortOrder[0])?.value || ""
+ }
+ onPress={handleShowSortOrderFilter}
+ />
+
+
+ {/* Grid */}
+
+
+ {t("search.no_results")}
+
+
+ }
+ contentInsetAdjustmentBehavior='automatic'
+ data={flatData}
+ renderItem={renderTVItem}
+ extraData={[orientation, nrOfCols]}
+ keyExtractor={keyExtractor}
+ numColumns={nrOfCols}
+ removeClippedSubviews={false}
+ onEndReached={() => {
+ if (hasNextPage) {
+ fetchNextPage();
+ }
+ }}
+ onEndReachedThreshold={1}
+ contentContainerStyle={{
+ paddingBottom: 24,
+ paddingLeft: TV_SCALE_PADDING,
+ paddingRight: TV_SCALE_PADDING,
+ paddingTop: 20,
+ }}
+ ItemSeparatorComponent={() => (
+
+ )}
+ />
+
);
};
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/items/page.tsx
index d61072177..54fe92126 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/items/page.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/items/page.tsx
@@ -3,9 +3,8 @@ import { useLocalSearchParams } from "expo-router";
import type React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
-import { View } from "react-native";
+import { Platform, View } from "react-native";
import Animated, {
- runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
@@ -15,6 +14,10 @@ import { ItemContent } from "@/components/ItemContent";
import { useItemQuery } from "@/hooks/useItemQuery";
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
+const ItemContentSkeletonTV = Platform.isTV
+ ? require("@/components/ItemContentSkeleton.tv").ItemContentSkeletonTV
+ : null;
+
const Page: React.FC = () => {
const { id } = useLocalSearchParams() as { id: string };
const { t } = useTranslation();
@@ -24,14 +27,20 @@ const Page: React.FC = () => {
// Exclude MediaSources/MediaStreams from initial fetch for faster loading
// (especially important for plugins like Gelato)
- const { data: item, isError } = useItemQuery(id, isOffline, undefined, [
+ const {
+ data: item,
+ isError,
+ isLoading,
+ } = useItemQuery(id, isOffline, undefined, [
ItemFields.MediaSources,
ItemFields.MediaSourceCount,
ItemFields.MediaStreams,
]);
- // Lazily preload item with full media sources in background
- const { data: itemWithSources } = useItemQuery(id, isOffline, undefined, []);
+ // Lazily preload item with full media sources in background — never cache
+ const { data: itemWithSources } = useItemQuery(id, isOffline, undefined, [], {
+ gcTime: 0,
+ });
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => {
@@ -40,33 +49,14 @@ const Page: React.FC = () => {
};
});
- const fadeOut = (callback: any) => {
- setTimeout(() => {
- opacity.value = withTiming(0, { duration: 500 }, (finished) => {
- if (finished) {
- runOnJS(callback)();
- }
- });
- }, 100);
- };
-
- const fadeIn = (callback: any) => {
- setTimeout(() => {
- opacity.value = withTiming(1, { duration: 500 }, (finished) => {
- if (finished) {
- runOnJS(callback)();
- }
- });
- }, 100);
- };
-
+ // Fast fade out when item loads (no setTimeout delay)
useEffect(() => {
if (item) {
- fadeOut(() => {});
+ opacity.value = withTiming(0, { duration: 150 });
} else {
- fadeIn(() => {});
+ opacity.value = withTiming(1, { duration: 150 });
}
- }, [item]);
+ }, [item, opacity]);
if (isError)
return (
@@ -78,31 +68,46 @@ const Page: React.FC = () => {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {item && }
+ {/* Always render ItemContent - it handles loading state internally on TV */}
+
+
+ {/* Skeleton overlay - fades out when content loads */}
+ {!item && (
+
+ {Platform.isTV && ItemContentSkeletonTV ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )}
);
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/company/[companyId].tsx
index fdcd786c9..34d83446c 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/company/[companyId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/company/[companyId].tsx
@@ -13,7 +13,7 @@ import {
} from "@/utils/jellyseerr/server/models/Search";
import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
-export default function page() {
+export default function JellyseerrCompanyPage() {
const local = useLocalSearchParams();
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/genre/[genreId].tsx
index 7ea008085..e8ac35dd7 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/genre/[genreId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/genre/[genreId].tsx
@@ -9,7 +9,7 @@ import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
-export default function page() {
+export default function JellyseerrGenrePage() {
const local = useLocalSearchParams();
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx
index 2f0af5949..519d5e5cc 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx
@@ -21,6 +21,7 @@ import { GenreTags } from "@/components/GenreTags";
import Cast from "@/components/jellyseerr/Cast";
import DetailFacts from "@/components/jellyseerr/DetailFacts";
import RequestModal from "@/components/jellyseerr/RequestModal";
+import { TVJellyseerrPage } from "@/components/jellyseerr/tv";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { PlatformDropdown } from "@/components/PlatformDropdown";
@@ -52,7 +53,8 @@ import type {
} from "@/utils/jellyseerr/server/models/Search";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
-const Page: React.FC = () => {
+// Mobile page component
+const MobilePage: React.FC = () => {
const insets = useSafeAreaInsets();
const params = useLocalSearchParams();
const { t } = useTranslation();
@@ -542,4 +544,12 @@ const Page: React.FC = () => {
);
};
+// Platform-conditional page component
+const Page: React.FC = () => {
+ if (Platform.isTV) {
+ return ;
+ }
+ return ;
+};
+
export default Page;
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/person/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/person/[personId].tsx
index a29e12809..c94a71bd4 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/person/[personId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/person/[personId].tsx
@@ -11,7 +11,7 @@ import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
-export default function page() {
+export default function JellyseerrPersonPage() {
const local = useLocalSearchParams();
const { t } = useTranslation();
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/_layout.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/_layout.tsx
index 072b2f931..f5bdc3e3b 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/_layout.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/_layout.tsx
@@ -1,13 +1,14 @@
+import { Slot, Stack, withLayoutContext } from "expo-router";
import {
createMaterialTopTabNavigator,
MaterialTopTabNavigationEventMap,
MaterialTopTabNavigationOptions,
-} from "@react-navigation/material-top-tabs";
+} from "expo-router/js-top-tabs";
import type {
ParamListBase,
TabNavigationState,
-} from "@react-navigation/native";
-import { Stack, withLayoutContext } from "expo-router";
+} from "expo-router/react-navigation";
+import { Platform } from "react-native";
const { Navigator } = createMaterialTopTabNavigator();
@@ -19,6 +20,17 @@ export const Tab = withLayoutContext<
>(Navigator);
const Layout = () => {
+ // On TV, skip the Material Top Tab Navigator and render children directly
+ // The TV version handles its own tab navigation internally
+ if (Platform.isTV) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
return (
<>
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/channels.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/channels.tsx
index 6c9790b59..98b712acc 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/channels.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/channels.tsx
@@ -8,7 +8,7 @@ import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-export default function page() {
+export default function LiveTvChannelsPage() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const _insets = useSafeAreaInsets();
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/guide.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/guide.tsx
index 390e8eb60..a69318e68 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/guide.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/guide.tsx
@@ -17,7 +17,7 @@ const ITEMS_PER_PAGE = 20;
const MemoizedLiveTVGuideRow = React.memo(LiveTVGuideRow);
-export default function page() {
+export default function LiveTvGuidePage() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/programs.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/programs.tsx
index 812d084d9..f1471e3a0 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/programs.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/programs.tsx
@@ -2,12 +2,21 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
import { useAtom } from "jotai";
import { useTranslation } from "react-i18next";
-import { ScrollView, View } from "react-native";
+import { Platform, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
+import { TVLiveTVPage } from "@/components/livetv/TVLiveTVPage";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
export default function page() {
+ if (Platform.isTV) {
+ return ;
+ }
+
+ return ;
+}
+
+function MobileLiveTVPrograms() {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const insets = useSafeAreaInsets();
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/recordings.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/recordings.tsx
index 9a390162a..cc482c557 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/recordings.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/livetv/recordings.tsx
@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
-export default function page() {
+export default function LiveTvRecordingsPage() {
const { t } = useTranslation();
return (
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/persons/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/persons/[personId].tsx
index f2f8dcafe..a6dde1e0a 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/persons/[personId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/persons/[personId].tsx
@@ -6,7 +6,7 @@ import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
-import { View } from "react-native";
+import { Platform, View } from "react-native";
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorizontalScroll";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
@@ -15,6 +15,7 @@ import { Loader } from "@/components/Loader";
import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
+import { TVActorPage } from "@/components/persons/TVActorPage";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
@@ -23,6 +24,16 @@ import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
const page: React.FC = () => {
const local = useLocalSearchParams();
const { personId } = local as { personId: string };
+
+ // Render TV-optimized page on TV platforms
+ if (Platform.isTV) {
+ return ;
+ }
+
+ return ;
+};
+
+const MobileActorPage: React.FC<{ personId: string }> = ({ personId }) => {
const { t } = useTranslation();
const [api] = useAtom(apiAtom);
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/series/[id].tsx
index 9c2d3f486..cc4b21b98 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/series/[id].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/series/[id].tsx
@@ -14,6 +14,7 @@ import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { SeriesHeader } from "@/components/series/SeriesHeader";
+import { TVSeriesPage } from "@/components/series/TVSeriesPage";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
@@ -61,6 +62,7 @@ const page: React.FC = () => {
});
},
staleTime: isOffline ? Infinity : 60 * 1000,
+ refetchInterval: !isOffline && Platform.isTV ? 60 * 1000 : undefined,
enabled: isOffline || (!!api && !!user?.Id),
});
@@ -116,7 +118,8 @@ const page: React.FC = () => {
(a.ParentIndexNumber ?? 0) - (b.ParentIndexNumber ?? 0) ||
(a.IndexNumber ?? 0) - (b.IndexNumber ?? 0),
),
- staleTime: isOffline ? Infinity : 60,
+ staleTime: isOffline ? Infinity : 60 * 1000,
+ refetchInterval: !isOffline && Platform.isTV ? 60 * 1000 : undefined,
enabled: isOffline || (!!api && !!user?.Id),
});
@@ -159,6 +162,19 @@ const page: React.FC = () => {
// For offline mode, we can show the page even without backdropUrl
if (!item || (!isOffline && !backdropUrl)) return null;
+ // TV version
+ if (Platform.isTV) {
+ return (
+
+
+
+ );
+ }
+
return (
{
const searchParams = useLocalSearchParams() as {
@@ -58,6 +83,8 @@ const Page = () => {
};
const { libraryId } = searchParams;
+ const typography = useScaledTVTypography();
+ const posterSizes = useScaledTVPosterSizes();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { width: screenWidth } = useWindowDimensions();
@@ -78,7 +105,54 @@ const Page = () => {
const { orientation } = useOrientation();
+ // Fallback refresh for newly added content when returning to the library
+ // (primary path is the LibraryChanged WebSocket event).
+ useRefreshLibraryOnFocus();
+
const { t } = useTranslation();
+ const router = useRouter();
+ const { showOptions } = useTVOptionModal();
+ const { showItemActions } = useTVItemActionModal();
+
+ // TV Filter queries
+ const { data: tvGenreOptions } = useQuery({
+ queryKey: ["filters", "Genres", "tvGenreFilter", libraryId],
+ queryFn: async () => {
+ if (!api) return [];
+ const response = await getFilterApi(api).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: libraryId,
+ });
+ return response.data.Genres || [];
+ },
+ enabled: Platform.isTV && !!api && !!user?.Id && !!libraryId,
+ });
+
+ const { data: tvYearOptions } = useQuery({
+ queryKey: ["filters", "Years", "tvYearFilter", libraryId],
+ queryFn: async () => {
+ if (!api) return [];
+ const response = await getFilterApi(api).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: libraryId,
+ });
+ return response.data.Years || [];
+ },
+ enabled: Platform.isTV && !!api && !!user?.Id && !!libraryId,
+ });
+
+ const { data: tvTagOptions } = useQuery({
+ queryKey: ["filters", "Tags", "tvTagFilter", libraryId],
+ queryFn: async () => {
+ if (!api) return [];
+ const response = await getFilterApi(api).getQueryFiltersLegacy({
+ userId: user?.Id,
+ parentId: libraryId,
+ });
+ return response.data.Tags || [];
+ },
+ enabled: Platform.isTV && !!api && !!user?.Id && !!libraryId,
+ });
useEffect(() => {
// Check for URL params first (from "See All" navigation)
@@ -162,6 +236,10 @@ const Page = () => {
);
const nrOfCols = useMemo(() => {
+ if (Platform.isTV) {
+ // TV uses flexWrap, so nrOfCols is just for mobile
+ return 1;
+ }
if (screenWidth < 300) return 2;
if (screenWidth < 500) return 3;
if (screenWidth < 800) return 5;
@@ -213,6 +291,8 @@ const Page = () => {
itemType = "Video";
} else if (library.CollectionType === "musicvideos") {
itemType = "MusicVideo";
+ } else if (library.CollectionType === "playlists") {
+ itemType = "Playlist";
}
const response = await getItemsApi(api).getItems({
@@ -232,6 +312,9 @@ const Page = () => {
tags: selectedTags,
years: selectedYears.map((year) => Number.parseInt(year, 10)),
includeItemTypes: itemType ? [itemType] : undefined,
+ ...(Platform.isTV && library.CollectionType === "playlists"
+ ? { mediaTypes: ["Video"] }
+ : {}),
});
return response.data || null;
@@ -322,7 +405,88 @@ const Page = () => {
),
- [orientation],
+ [orientation, nrOfCols],
+ );
+
+ const renderTVItem = useCallback(
+ (item: BaseItemDto) => {
+ const handlePress = () => {
+ if (item.Type === "Playlist") {
+ router.push({
+ pathname: "/(auth)/(tabs)/(libraries)/[libraryId]",
+ params: { libraryId: item.Id! },
+ });
+ return;
+ }
+ const navTarget = getItemNavigation(item, "(libraries)");
+ router.push(navTarget as any);
+ };
+
+ // Special rendering for Playlist items (square thumbnails)
+ if (item.Type === "Playlist") {
+ const playlistImageUrl = getPrimaryImageUrl({
+ api,
+ item,
+ width: TV_PLAYLIST_SQUARE_SIZE * 2,
+ });
+
+ return (
+
+ showItemActions(item)}
+ >
+
+
+
+
+
+
+ {item.Name}
+
+
+
+ );
+ }
+
+ return (
+ showItemActions(item)}
+ width={posterSizes.poster}
+ />
+ );
+ },
+ [router, showItemActions, api, typography],
);
const keyExtractor = useCallback((item: BaseItemDto) => item.Id || "", []);
@@ -509,6 +673,188 @@ const Page = () => {
],
);
+ // TV Filter bar header
+ const hasActiveFilters =
+ selectedGenres.length > 0 ||
+ selectedYears.length > 0 ||
+ selectedTags.length > 0 ||
+ filterBy.length > 0;
+
+ const resetAllFilters = useCallback(() => {
+ setSelectedGenres([]);
+ setSelectedYears([]);
+ setSelectedTags([]);
+ _setFilterBy([]);
+ }, [setSelectedGenres, setSelectedYears, setSelectedTags, _setFilterBy]);
+
+ // TV Filter options - with "All" option for clearable filters
+ const tvGenreFilterOptions = useMemo(
+ (): TVOptionItem[] => [
+ {
+ label: t("library.filters.all"),
+ value: "__all__",
+ selected: selectedGenres.length === 0,
+ },
+ ...(tvGenreOptions || []).map((genre) => ({
+ label: genre,
+ value: genre,
+ selected: selectedGenres.includes(genre),
+ })),
+ ],
+ [tvGenreOptions, selectedGenres, t],
+ );
+
+ const tvYearFilterOptions = useMemo(
+ (): TVOptionItem[] => [
+ {
+ label: t("library.filters.all"),
+ value: "__all__",
+ selected: selectedYears.length === 0,
+ },
+ ...(tvYearOptions || []).map((year) => ({
+ label: String(year),
+ value: String(year),
+ selected: selectedYears.includes(String(year)),
+ })),
+ ],
+ [tvYearOptions, selectedYears, t],
+ );
+
+ const tvTagFilterOptions = useMemo(
+ (): TVOptionItem[] => [
+ {
+ label: t("library.filters.all"),
+ value: "__all__",
+ selected: selectedTags.length === 0,
+ },
+ ...(tvTagOptions || []).map((tag) => ({
+ label: tag,
+ value: tag,
+ selected: selectedTags.includes(tag),
+ })),
+ ],
+ [tvTagOptions, selectedTags, t],
+ );
+
+ const tvSortByOptions = useMemo(
+ (): TVOptionItem[] =>
+ sortOptions.map((option) => ({
+ label: option.value,
+ value: option.key,
+ selected: sortBy[0] === option.key,
+ })),
+ [sortBy],
+ );
+
+ const tvSortOrderOptions = useMemo(
+ (): TVOptionItem[] =>
+ sortOrderOptions.map((option) => ({
+ label: option.value,
+ value: option.key,
+ selected: sortOrder[0] === option.key,
+ })),
+ [sortOrder],
+ );
+
+ const tvFilterByOptions = useMemo(
+ (): TVOptionItem[] => [
+ {
+ label: t("library.filters.all"),
+ value: "__all__",
+ selected: filterBy.length === 0,
+ },
+ ...generalFilters.map((option) => ({
+ label: option.value,
+ value: option.key,
+ selected: filterBy.includes(option.key),
+ })),
+ ],
+ [filterBy, generalFilters, t],
+ );
+
+ // TV Filter handlers using navigation-based modal
+ const handleShowGenreFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.genres"),
+ options: tvGenreFilterOptions,
+ onSelect: (value: string) => {
+ if (value === "__all__") {
+ setSelectedGenres([]);
+ } else if (selectedGenres.includes(value)) {
+ setSelectedGenres(selectedGenres.filter((g) => g !== value));
+ } else {
+ setSelectedGenres([...selectedGenres, value]);
+ }
+ },
+ });
+ }, [showOptions, t, tvGenreFilterOptions, selectedGenres, setSelectedGenres]);
+
+ const handleShowYearFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.years"),
+ options: tvYearFilterOptions,
+ onSelect: (value: string) => {
+ if (value === "__all__") {
+ setSelectedYears([]);
+ } else if (selectedYears.includes(value)) {
+ setSelectedYears(selectedYears.filter((y) => y !== value));
+ } else {
+ setSelectedYears([...selectedYears, value]);
+ }
+ },
+ });
+ }, [showOptions, t, tvYearFilterOptions, selectedYears, setSelectedYears]);
+
+ const handleShowTagFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.tags"),
+ options: tvTagFilterOptions,
+ onSelect: (value: string) => {
+ if (value === "__all__") {
+ setSelectedTags([]);
+ } else if (selectedTags.includes(value)) {
+ setSelectedTags(selectedTags.filter((tag) => tag !== value));
+ } else {
+ setSelectedTags([...selectedTags, value]);
+ }
+ },
+ });
+ }, [showOptions, t, tvTagFilterOptions, selectedTags, setSelectedTags]);
+
+ const handleShowSortByFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.sort_by"),
+ options: tvSortByOptions,
+ onSelect: (value: SortByOption) => {
+ setSortBy([value]);
+ },
+ });
+ }, [showOptions, t, tvSortByOptions, setSortBy]);
+
+ const handleShowSortOrderFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.sort_order"),
+ options: tvSortOrderOptions,
+ onSelect: (value: SortOrderOption) => {
+ setSortOrder([value]);
+ },
+ });
+ }, [showOptions, t, tvSortOrderOptions, setSortOrder]);
+
+ const handleShowFilterByFilter = useCallback(() => {
+ showOptions({
+ title: t("library.filters.filter_by"),
+ options: tvFilterByOptions,
+ onSelect: (value: string) => {
+ if (value === "__all__") {
+ _setFilterBy([]);
+ } else {
+ setFilter([value as FilterByOption]);
+ }
+ },
+ });
+ }, [showOptions, t, tvFilterByOptions, setFilter, _setFilterBy]);
+
const insets = useSafeAreaInsets();
if (isLoading || isLibraryLoading)
@@ -518,43 +864,176 @@ const Page = () => {
);
+ // Mobile return
+ if (!Platform.isTV) {
+ return (
+
+
+ {t("library.no_results")}
+
+
+ }
+ contentInsetAdjustmentBehavior='automatic'
+ data={flatData}
+ renderItem={renderItem}
+ extraData={[orientation, nrOfCols]}
+ keyExtractor={keyExtractor}
+ numColumns={nrOfCols}
+ onEndReached={() => {
+ if (hasNextPage) {
+ fetchNextPage();
+ }
+ }}
+ onEndReachedThreshold={1}
+ ListHeaderComponent={ListHeaderComponent}
+ contentContainerStyle={{
+ paddingBottom: 24,
+ paddingLeft: insets.left,
+ paddingRight: insets.right,
+ }}
+ ItemSeparatorComponent={() => (
+
+ )}
+ />
+ );
+ }
+
+ // TV return with filter bar
return (
-
-
- {t("library.no_results")}
-
-
- }
- contentInsetAdjustmentBehavior='automatic'
- data={flatData}
- renderItem={renderItem}
- extraData={[orientation, nrOfCols]}
- keyExtractor={keyExtractor}
- numColumns={nrOfCols}
- onEndReached={() => {
- if (hasNextPage) {
+ {
+ // Load more when near bottom
+ const { layoutMeasurement, contentOffset, contentSize } = nativeEvent;
+ const isNearBottom =
+ layoutMeasurement.height + contentOffset.y >=
+ contentSize.height - 500;
+ if (isNearBottom && hasNextPage && !isFetching) {
fetchNextPage();
}
}}
- onEndReachedThreshold={1}
- ListHeaderComponent={ListHeaderComponent}
- contentContainerStyle={{
- paddingBottom: 24,
- paddingLeft: insets.left,
- paddingRight: insets.right,
- }}
- ItemSeparatorComponent={() => (
+ scrollEventThrottle={400}
+ >
+ {/* Filter bar */}
+
+ {hasActiveFilters && (
+
+ )}
+ 0
+ ? `${selectedGenres.length} selected`
+ : t("library.filters.all")
+ }
+ onPress={handleShowGenreFilter}
+ hasTVPreferredFocus={!hasActiveFilters}
+ hasActiveFilter={selectedGenres.length > 0}
+ />
+ 0
+ ? `${selectedYears.length} selected`
+ : t("library.filters.all")
+ }
+ onPress={handleShowYearFilter}
+ hasActiveFilter={selectedYears.length > 0}
+ />
+ 0
+ ? `${selectedTags.length} selected`
+ : t("library.filters.all")
+ }
+ onPress={handleShowTagFilter}
+ hasActiveFilter={selectedTags.length > 0}
+ />
+ o.key === sortBy[0])?.value || ""}
+ onPress={handleShowSortByFilter}
+ />
+ o.key === sortOrder[0])?.value || ""
+ }
+ onPress={handleShowSortOrderFilter}
+ />
+ 0
+ ? generalFilters.find((o) => o.key === filterBy[0])?.value || ""
+ : t("library.filters.all")
+ }
+ onPress={handleShowFilterByFilter}
+ hasActiveFilter={filterBy.length > 0}
+ />
+
+
+ {/* Grid with flexWrap */}
+ {flatData.length === 0 ? (
+ >
+
+ {t("library.no_results")}
+
+
+ ) : (
+
+ {flatData.map((item) => renderTVItem(item))}
+
)}
- />
+
+ {/* Loading indicator */}
+ {isFetching && (
+
+
+
+ )}
+
);
};
diff --git a/app/(auth)/(tabs)/(libraries)/index.tsx b/app/(auth)/(tabs)/(libraries)/index.tsx
index 37b89bbf2..481503cf8 100644
--- a/app/(auth)/(tabs)/(libraries)/index.tsx
+++ b/app/(auth)/(tabs)/(libraries)/index.tsx
@@ -1,109 +1,11 @@
-import {
- getUserLibraryApi,
- getUserViewsApi,
-} from "@jellyfin/sdk/lib/utils/api";
-import { FlashList } from "@shopify/flash-list";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useAtom } from "jotai";
-import { useEffect, useMemo } from "react";
-import { useTranslation } from "react-i18next";
-import { Platform, StyleSheet, View } from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { Text } from "@/components/common/Text";
-import { Loader } from "@/components/Loader";
-import { LibraryItemCard } from "@/components/library/LibraryItemCard";
-import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-import { useSettings } from "@/utils/atoms/settings";
+import { Platform } from "react-native";
+import { Libraries } from "@/components/library/Libraries";
+import { TVLibraries } from "@/components/library/TVLibraries";
-export default function index() {
- const [api] = useAtom(apiAtom);
- const [user] = useAtom(userAtom);
- const queryClient = useQueryClient();
- const { settings } = useSettings();
+export default function LibrariesPage() {
+ if (Platform.isTV) {
+ return ;
+ }
- const { t } = useTranslation();
-
- const { data, isLoading } = useQuery({
- queryKey: ["user-views", user?.Id],
- queryFn: async () => {
- const response = await getUserViewsApi(api!).getUserViews({
- userId: user?.Id,
- });
-
- return response.data.Items || null;
- },
- staleTime: 60,
- });
-
- const libraries = useMemo(
- () =>
- data
- ?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!))
- .filter((l) => l.CollectionType !== "books") || [],
- [data, settings?.hiddenLibraries],
- );
-
- useEffect(() => {
- for (const item of data || []) {
- queryClient.prefetchQuery({
- queryKey: ["library", item.Id],
- queryFn: async () => {
- if (!item.Id || !user?.Id || !api) return null;
- const response = await getUserLibraryApi(api).getItem({
- itemId: item.Id,
- userId: user?.Id,
- });
- return response.data;
- },
- staleTime: 60 * 1000,
- });
- }
- }, [data]);
-
- const insets = useSafeAreaInsets();
-
- if (isLoading)
- return (
-
-
-
- );
-
- if (!libraries)
- return (
-
-
- {t("library.no_libraries_found")}
-
-
- );
-
- return (
- }
- keyExtractor={(item) => item.Id || ""}
- ItemSeparatorComponent={() =>
- settings?.libraryOptions?.display === "row" ? (
-
- ) : (
-
- )
- }
- />
- );
+ return ;
}
diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx
index 69daf9296..66f877d4a 100644
--- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx
+++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/_layout.tsx
@@ -1,13 +1,13 @@
+import { Stack, useLocalSearchParams, withLayoutContext } from "expo-router";
import {
createMaterialTopTabNavigator,
MaterialTopTabNavigationEventMap,
MaterialTopTabNavigationOptions,
-} from "@react-navigation/material-top-tabs";
+} from "expo-router/js-top-tabs";
import type {
ParamListBase,
TabNavigationState,
-} from "@react-navigation/native";
-import { Stack, useLocalSearchParams, withLayoutContext } from "expo-router";
+} from "expo-router/react-navigation";
import { useTranslation } from "react-i18next";
const { Navigator } = createMaterialTopTabNavigator();
diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx
index 3fb5305b2..10fffbe71 100644
--- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx
+++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/albums.tsx
@@ -1,8 +1,8 @@
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
-import { useRoute } from "@react-navigation/native";
import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
+import { useRoute } from "expo-router/react-navigation";
import { useAtom } from "jotai";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx
index e8191404c..f268e0b24 100644
--- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx
+++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx
@@ -1,8 +1,8 @@
import { getArtistsApi } from "@jellyfin/sdk/lib/utils/api";
-import { useRoute } from "@react-navigation/native";
import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
+import { useRoute } from "expo-router/react-navigation";
import { useAtom } from "jotai";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx
index a03f9c3db..85b4be5fd 100644
--- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx
+++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx
@@ -1,9 +1,9 @@
import { Ionicons } from "@expo/vector-icons";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
-import { useNavigation, useRoute } from "@react-navigation/native";
import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
+import { useNavigation, useRoute } from "expo-router/react-navigation";
import { useAtom } from "jotai";
import { useCallback, useLayoutEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx
index fb762862d..81c2272f8 100644
--- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx
+++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx
@@ -1,9 +1,9 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
-import { useRoute } from "@react-navigation/native";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
+import { useRoute } from "expo-router/react-navigation";
import { useAtom } from "jotai";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx
index 751b1df10..29461b49a 100644
--- a/app/(auth)/(tabs)/(search)/index.tsx
+++ b/app/(auth)/(tabs)/(search)/index.tsx
@@ -7,8 +7,9 @@ import { useAsyncDebouncer } from "@tanstack/react-pacer";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { Image } from "expo-image";
-import { useLocalSearchParams, useNavigation } from "expo-router";
+import { useLocalSearchParams, useNavigation, useSegments } from "expo-router";
import { useAtom } from "jotai";
+import { orderBy, uniqBy } from "lodash";
import {
useCallback,
useEffect,
@@ -22,9 +23,11 @@ import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
-import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
-import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
+import {
+ getItemNavigation,
+ TouchableItemRouter,
+} from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText";
import {
JellyseerrSearchSort,
@@ -36,12 +39,20 @@ import { DiscoverFilters } from "@/components/search/DiscoverFilters";
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
import { SearchTabButtons } from "@/components/search/SearchTabButtons";
+import { TVSearchPage } from "@/components/search/TVSearchPage";
import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr";
+import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
+import { MediaType } from "@/utils/jellyseerr/server/constants/media";
+import type {
+ MovieResult,
+ PersonResult,
+ TvResult,
+} from "@/utils/jellyseerr/server/models/Search";
import { createStreamystatsApi } from "@/utils/streamystats";
type SearchType = "Library" | "Discover";
@@ -55,10 +66,13 @@ const exampleSearches = [
"The Mandalorian",
];
-export default function search() {
+export default function SearchPage() {
const params = useLocalSearchParams();
const insets = useSafeAreaInsets();
const router = useRouter();
+ const { showItemActions } = useTVItemActionModal();
+ const segments = useSegments();
+ const from = (segments as string[])[2] || "(search)";
const [user] = useAtom(userAtom);
@@ -199,9 +213,7 @@ export default function search() {
return [];
}
- const url = `${
- settings.marlinServerUrl
- }/search?q=${encodeURIComponent(query)}&includeItemTypes=${types
+ const url = `${settings.marlinServerUrl}/search?q=${encodeURIComponent(query)}&includeItemTypes=${types
.map((type) => encodeURIComponent(type))
.join("&includeItemTypes=")}`;
@@ -209,7 +221,7 @@ export default function search() {
const ids = response1.data.ids;
- if (!ids || !ids.length) {
+ if (!ids?.length) {
return [];
}
@@ -293,6 +305,9 @@ export default function search() {
},
hideWhenScrolling: false,
autoFocus: false,
+ // Android: placeholder and icon color
+ hintTextColor: "#fff",
+ headerIconColor: "#fff",
},
});
}, [navigation]);
@@ -440,6 +455,180 @@ export default function search() {
return l1 || l2 || l3 || l7 || l8 || l9 || l10 || l11 || l12;
}, [l1, l2, l3, l7, l8, l9, l10, l11, l12]);
+ // TV item press handler
+ const handleItemPress = useCallback(
+ (item: BaseItemDto) => {
+ const navigation = getItemNavigation(item, from);
+ router.push(navigation as any);
+ },
+ [from, router],
+ );
+
+ // Jellyseerr search for TV
+ const { data: jellyseerrTVResults, isFetching: jellyseerrTVLoading } =
+ useQuery({
+ queryKey: ["search", "jellyseerr", "tv", debouncedSearch],
+ queryFn: async () => {
+ const params = {
+ query: new URLSearchParams(debouncedSearch || "").toString(),
+ };
+ return await Promise.all([
+ jellyseerrApi?.search({ ...params, page: 1 }),
+ jellyseerrApi?.search({ ...params, page: 2 }),
+ jellyseerrApi?.search({ ...params, page: 3 }),
+ jellyseerrApi?.search({ ...params, page: 4 }),
+ ]).then((all) =>
+ uniqBy(
+ all.flatMap((v) => v?.results || []),
+ "id",
+ ),
+ );
+ },
+ enabled:
+ Platform.isTV &&
+ !!jellyseerrApi &&
+ searchType === "Discover" &&
+ debouncedSearch.length > 0,
+ });
+
+ // Process Jellyseerr results for TV
+ const jellyseerrMovieResults = useMemo(
+ () =>
+ orderBy(
+ jellyseerrTVResults?.filter(
+ (r) => r.mediaType === MediaType.MOVIE,
+ ) as MovieResult[],
+ [(m) => m?.title?.toLowerCase() === debouncedSearch.toLowerCase()],
+ "desc",
+ ),
+ [jellyseerrTVResults, debouncedSearch],
+ );
+
+ const jellyseerrTvResults = useMemo(
+ () =>
+ orderBy(
+ jellyseerrTVResults?.filter(
+ (r) => r.mediaType === MediaType.TV,
+ ) as TvResult[],
+ [(t) => t?.name?.toLowerCase() === debouncedSearch.toLowerCase()],
+ "desc",
+ ),
+ [jellyseerrTVResults, debouncedSearch],
+ );
+
+ const jellyseerrPersonResults = useMemo(
+ () =>
+ orderBy(
+ jellyseerrTVResults?.filter(
+ (r) => r.mediaType === "person",
+ ) as PersonResult[],
+ [(p) => p?.name?.toLowerCase() === debouncedSearch.toLowerCase()],
+ "desc",
+ ),
+ [jellyseerrTVResults, debouncedSearch],
+ );
+
+ const jellyseerrTVNoResults = useMemo(() => {
+ return (
+ !jellyseerrMovieResults?.length &&
+ !jellyseerrTvResults?.length &&
+ !jellyseerrPersonResults?.length
+ );
+ }, [jellyseerrMovieResults, jellyseerrTvResults, jellyseerrPersonResults]);
+
+ // Fetch discover settings for TV (when no search query in Discover mode)
+ const { data: discoverSliders } = useQuery({
+ queryKey: ["search", "jellyseerr", "discoverSettings", "tv"],
+ queryFn: async () => jellyseerrApi?.discoverSettings(),
+ enabled:
+ Platform.isTV &&
+ !!jellyseerrApi &&
+ searchType === "Discover" &&
+ debouncedSearch.length === 0,
+ });
+
+ // TV Jellyseerr press handlers
+ const handleJellyseerrMoviePress = useCallback(
+ (item: MovieResult) => {
+ router.push({
+ pathname: "/(auth)/(tabs)/(search)/jellyseerr/page",
+ params: {
+ mediaTitle: item.title,
+ releaseYear: String(new Date(item.releaseDate || "").getFullYear()),
+ canRequest: "true",
+ posterSrc: jellyseerrApi?.imageProxy(item.posterPath) || "",
+ mediaType: MediaType.MOVIE,
+ id: String(item.id),
+ backdropPath: item.backdropPath || "",
+ overview: item.overview || "",
+ },
+ });
+ },
+ [router, jellyseerrApi],
+ );
+
+ const handleJellyseerrTvPress = useCallback(
+ (item: TvResult) => {
+ router.push({
+ pathname: "/(auth)/(tabs)/(search)/jellyseerr/page",
+ params: {
+ mediaTitle: item.name,
+ releaseYear: String(new Date(item.firstAirDate || "").getFullYear()),
+ canRequest: "true",
+ posterSrc: jellyseerrApi?.imageProxy(item.posterPath) || "",
+ mediaType: MediaType.TV,
+ id: String(item.id),
+ backdropPath: item.backdropPath || "",
+ overview: item.overview || "",
+ },
+ });
+ },
+ [router, jellyseerrApi],
+ );
+
+ const handleJellyseerrPersonPress = useCallback(
+ (item: PersonResult) => {
+ router.push(`/(auth)/jellyseerr/person/${item.id}` as any);
+ },
+ [router],
+ );
+
+ // Render TV search page
+ if (Platform.isTV) {
+ return (
+
+ );
+ }
+
return (
- {/* */}
- {Platform.isTV && (
- {
- router.setParams({ q: "" });
- setSearch(text);
- }}
- keyboardType='default'
- returnKeyType='done'
- autoCapitalize='none'
- clearButtonMode='while-editing'
- maxLength={500}
- />
- )}
) : debouncedSearch.length === 0 ? (
-
+
{exampleSearches.map((e) => (
{
diff --git a/app/(auth)/(tabs)/(settings)/_layout.tsx b/app/(auth)/(tabs)/(settings)/_layout.tsx
new file mode 100644
index 000000000..4f1ce0354
--- /dev/null
+++ b/app/(auth)/(tabs)/(settings)/_layout.tsx
@@ -0,0 +1,21 @@
+import { Stack } from "expo-router";
+import { useTranslation } from "react-i18next";
+import { Platform } from "react-native";
+
+export default function SettingsLayout() {
+ const { t } = useTranslation();
+ return (
+
+
+
+ );
+}
diff --git a/app/(auth)/(tabs)/(settings)/index.tsx b/app/(auth)/(tabs)/(settings)/index.tsx
new file mode 100644
index 000000000..52b86fb47
--- /dev/null
+++ b/app/(auth)/(tabs)/(settings)/index.tsx
@@ -0,0 +1,5 @@
+import SettingsTV from "@/app/(auth)/(tabs)/(home)/settings.tv";
+
+export default function SettingsTabScreen() {
+ return ;
+}
diff --git a/app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx b/app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx
index 831feac76..c649bdf62 100644
--- a/app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx
+++ b/app/(auth)/(tabs)/(watchlists)/[watchlistId].tsx
@@ -8,7 +8,9 @@ import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Alert,
+ Platform,
RefreshControl,
+ ScrollView,
TouchableOpacity,
useWindowDimensions,
View,
@@ -16,11 +18,18 @@ import {
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { HeaderBackButton } from "@/components/common/HeaderBackButton";
import { Text } from "@/components/common/Text";
-import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
+import {
+ getItemNavigation,
+ TouchableItemRouter,
+} from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText";
import { ItemPoster } from "@/components/posters/ItemPoster";
+import { TVPosterCard } from "@/components/tv/TVPosterCard";
+import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
+import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter";
import { useOrientation } from "@/hooks/useOrientation";
+import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
import {
useDeleteWatchlist,
useRemoveFromWatchlist,
@@ -32,9 +41,15 @@ import {
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { userAtom } from "@/providers/JellyfinProvider";
+const TV_ITEM_GAP = 20;
+const TV_HORIZONTAL_PADDING = 60;
+
export default function WatchlistDetailScreen() {
+ const typography = useScaledTVTypography();
+ const posterSizes = useScaledTVPosterSizes();
const { t } = useTranslation();
const router = useRouter();
+ const { showItemActions } = useTVItemActionModal();
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const { watchlistId } = useLocalSearchParams<{ watchlistId: string }>();
@@ -47,6 +62,8 @@ export default function WatchlistDetailScreen() {
: undefined;
const nrOfCols = useMemo(() => {
+ // TV uses flexWrap, so nrOfCols is just for mobile
+ if (Platform.isTV) return 1;
if (screenWidth < 300) return 2;
if (screenWidth < 500) return 3;
if (screenWidth < 800) return 5;
@@ -153,6 +170,28 @@ export default function WatchlistDetailScreen() {
[removeFromWatchlist, watchlistIdNum, watchlist?.name, t],
);
+ const renderTVItem = useCallback(
+ (item: BaseItemDto, index: number) => {
+ const handlePress = () => {
+ const navigation = getItemNavigation(item, "(watchlists)");
+ router.push(navigation as any);
+ };
+
+ return (
+ showItemActions(item)}
+ hasTVPreferredFocus={index === 0}
+ width={posterSizes.poster}
+ />
+ );
+ },
+ [router, showItemActions, posterSizes.poster],
+ );
+
const renderItem = useCallback(
({ item, index }: { item: BaseItemDto; index: number }) => (
+ {/* Header */}
+
+ {watchlist.description && (
+
+ {watchlist.description}
+
+ )}
+
+
+
+
+ {items?.length ?? 0}{" "}
+ {(items?.length ?? 0) === 1
+ ? t("watchlists.item")
+ : t("watchlists.items")}
+
+
+
+
+
+ {watchlist.isPublic
+ ? t("watchlists.public")
+ : t("watchlists.private")}
+
+
+ {!isOwner && (
+
+ {t("watchlists.by_owner")}
+
+ )}
+
+
+
+ {/* Grid with flexWrap */}
+ {!items || items.length === 0 ? (
+
+
+
+ {t("watchlists.empty_watchlist")}
+
+
+ ) : (
+
+ {items.map((item, index) => renderTVItem(item, index))}
+
+ )}
+
+ );
+ }
+
+ // Mobile layout with FlashList
return (
null
+ : require("@/components/music/MiniPlayerBar").MiniPlayerBar;
+const MusicPlaybackEngine = Platform.isTV
+ ? () => null
+ : require("@/components/music/MusicPlaybackEngine").MusicPlaybackEngine;
+
const { Navigator } = createNativeBottomTabNavigator();
export const NativeTabs = withLayoutContext<
@@ -30,6 +37,9 @@ export default function TabLayout() {
const { settings } = useSettings();
const { t } = useTranslation();
+ // Handle TV back button - prevent app exit when at root
+ useTVHomeBackHandler();
+
return (
@@ -117,6 +127,17 @@ export default function TabLayout() {
: (_e) => ({ sfSymbol: "list.dash.fill" }),
}}
/>
+ require("@/assets/icons/gear.png") //Should maybe use other libraries to have it uniform
+ : (_e) => ({ sfSymbol: "gearshape.fill" }),
+ }}
+ />
diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx
index 70b5cd755..0314a6e78 100644
--- a/app/(auth)/player/direct-player.tsx
+++ b/app/(auth)/player/direct-player.tsx
@@ -1,6 +1,7 @@
import {
type BaseItemDto,
type MediaSourceInfo,
+ type MediaStream,
PlaybackOrder,
PlaybackProgressInfo,
RepeatMode,
@@ -9,6 +10,7 @@ import {
getPlaystateApi,
getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api";
+import { File } from "expo-file-system";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtomValue } from "jotai";
@@ -20,6 +22,7 @@ import { BITRATES } from "@/components/BitrateSelector";
import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
import { Controls } from "@/components/video-player/controls/Controls";
+import { Controls as TVControls } from "@/components/video-player/controls/Controls.tv";
import { PlayerProvider } from "@/components/video-player/controls/contexts/PlayerContext";
import { VideoProvider } from "@/components/video-player/controls/contexts/VideoContext";
import {
@@ -43,11 +46,13 @@ import {
} from "@/modules";
import { useDownload } from "@/providers/DownloadProvider";
import { DownloadedItem } from "@/providers/Downloads/types";
+import { useInactivity } from "@/providers/InactivityProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-
import { OfflineModeProvider } from "@/providers/OfflineModeProvider";
+import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
import { useSettings } from "@/utils/atoms/settings";
+import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import {
@@ -55,10 +60,10 @@ import {
getMpvSubtitleId,
} from "@/utils/jellyfin/subtitleUtils";
import { writeToLog } from "@/utils/log";
-import { generateDeviceProfile } from "@/utils/profiles/native";
import { msToTicks, ticksToSeconds } from "@/utils/time";
+import { generateDeviceProfile } from "../../../utils/profiles/native";
-export default function page() {
+export default function DirectPlayerPage() {
const videoRef = useRef(null);
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
@@ -85,6 +90,12 @@ export default function page() {
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1.0);
const [showTechnicalInfo, setShowTechnicalInfo] = useState(false);
+ // TV audio/subtitle selection state (tracks current selection for dynamic changes)
+ const [currentAudioIndex, setCurrentAudioIndex] = useState<
+ number | undefined
+ >(undefined);
+ const [currentSubtitleIndex, setCurrentSubtitleIndex] = useState(-1);
+
const progress = useSharedValue(0);
const isSeeking = useSharedValue(false);
const cacheProgress = useSharedValue(0);
@@ -97,6 +108,9 @@ export default function page() {
// when data updates, only when the provider initializes
const downloadedFiles = downloadUtils.getDownloadedItems();
+ // Inactivity timer controls (TV only)
+ const { pauseInactivityTimer, resumeInactivityTimer } = useInactivity();
+
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
const lightHapticFeedback = useHaptic("light");
@@ -127,7 +141,6 @@ export default function page() {
const { lockOrientation, unlockOrientation } = useOrientation();
const offline = offlineStr === "true";
- const playbackManager = usePlaybackManager({ isOffline: offline });
// Audio index: use URL param if provided, otherwise use stored index for offline playback
// This is computed after downloadedItem is available, see audioIndexResolved below
@@ -136,12 +149,19 @@ export default function page() {
: undefined;
const subtitleIndexFromUrl = subtitleIndexStr
? Number.parseInt(subtitleIndexStr, 10)
- : -1;
+ : undefined;
const bitrateValue = bitrateValueStr
? Number.parseInt(bitrateValueStr, 10)
: BITRATES[0].value;
const [item, setItem] = useState(null);
+ const initialSeekDoneRef = useRef(false);
+
+ const initialPlaybackTicksRef = useRef(
+ playbackPositionFromUrl
+ ? Number.parseInt(playbackPositionFromUrl, 10)
+ : (item?.UserData?.PlaybackPositionTicks ?? 0),
+ );
const [downloadedItem, setDownloadedItem] = useState(
null,
);
@@ -150,6 +170,10 @@ export default function page() {
isError: false,
});
+ // Playback manager for progress reporting and adjacent items
+ const playbackManager = usePlaybackManager({ item, isOffline: offline });
+ const { nextItem, previousItem } = playbackManager;
+
// Resolve audio index: use URL param if provided, otherwise use stored index for offline playback
const audioIndex = useMemo(() => {
if (audioIndexFromUrl !== undefined) {
@@ -178,6 +202,16 @@ export default function page() {
offline,
downloadedItem?.userData?.subtitleStreamIndex,
]);
+ // Initialize TV audio/subtitle indices from URL params.
+ // No undefined guard: when a new episode's URL omits audioIndex, reset to
+ // undefined (media default) rather than leaking the previous episode's track.
+ useEffect(() => {
+ setCurrentAudioIndex(audioIndex);
+ }, [audioIndex]);
+
+ useEffect(() => {
+ setCurrentSubtitleIndex(subtitleIndex);
+ }, [subtitleIndex]);
// Get the playback speed for this item based on settings
const { playbackSpeed: initialPlaybackSpeed } = usePlaybackSpeed(
@@ -205,12 +239,25 @@ export default function page() {
);
/** Gets the initial playback position from the URL. */
- const getInitialPlaybackTicks = useCallback((): number => {
- if (playbackPositionFromUrl) {
- return Number.parseInt(playbackPositionFromUrl, 10);
+ // const getInitialPlaybackTicks = useCallback((): number => {
+ // if (playbackPositionFromUrl) {
+ // return Number.parseInt(playbackPositionFromUrl, 10);
+ // }
+ // return item?.UserData?.PlaybackPositionTicks ?? 0;
+ // }, [playbackPositionFromUrl, item?.UserData?.PlaybackPositionTicks]);
+
+ useEffect(() => {
+ if (!tracksReady || !videoRef.current) return;
+ if (initialSeekDoneRef.current) return;
+
+ initialSeekDoneRef.current = true;
+
+ const ticks = initialPlaybackTicksRef.current;
+
+ if (ticks > 0) {
+ videoRef.current.seekTo(ticksToSeconds(ticks));
}
- return item?.UserData?.PlaybackPositionTicks ?? 0;
- }, [playbackPositionFromUrl, item?.UserData?.PlaybackPositionTicks]);
+ }, [tracksReady]);
useEffect(() => {
const fetchItemData = async () => {
@@ -224,7 +271,12 @@ export default function page() {
setDownloadedItem(data);
}
} else {
- const res = await getUserLibraryApi(api!).getItem({
+ // Guard against api being null (e.g., during logout)
+ if (!api) {
+ setItemStatus({ isLoading: false, isError: false });
+ return;
+ }
+ const res = await getUserLibraryApi(api).getItem({
itemId,
userId: user?.Id,
});
@@ -258,6 +310,7 @@ export default function page() {
mediaSource: MediaSourceInfo;
sessionId: string;
url: string;
+ requiredHttpHeaders?: Record;
}
const [stream, setStream] = useState(null);
@@ -266,19 +319,22 @@ export default function page() {
isError: false,
});
+ // Ref to store the stream fetch function for refreshing subtitle tracks
+ const refetchStreamRef = useRef<(() => Promise) | null>(null);
+
useEffect(() => {
- const fetchStreamData = async () => {
+ const fetchStreamData = async (): Promise => {
setStreamStatus({ isLoading: true, isError: false });
try {
// Don't attempt to fetch stream data if item is not available
if (!item?.Id) {
console.log("Item not loaded yet, skipping stream data fetch");
setStreamStatus({ isLoading: false, isError: false });
- return;
+ return null;
}
let result: Stream | null = null;
- if (offline && downloadedItem && downloadedItem.mediaSource) {
+ if (offline && downloadedItem?.mediaSource) {
const url = downloadedItem.videoFilePath;
if (item) {
result = {
@@ -292,12 +348,12 @@ export default function page() {
if (!api) {
console.warn("API not available for streaming");
setStreamStatus({ isLoading: false, isError: true });
- return;
+ return null;
}
if (!user?.Id) {
console.warn("User not authenticated for streaming");
setStreamStatus({ isLoading: false, isError: true });
- return;
+ return null;
}
// Calculate start ticks directly from item to avoid stale closure
@@ -316,25 +372,30 @@ export default function page() {
subtitleStreamIndex: subtitleIndex,
deviceProfile: generateDeviceProfile(),
});
- if (!res) return;
- const { mediaSource, sessionId, url } = res;
+ if (!res) return null;
+ const { mediaSource, sessionId, url, requiredHttpHeaders } = res;
if (!sessionId || !mediaSource || !url) {
Alert.alert(
t("player.error"),
t("player.failed_to_get_stream_url"),
);
- return;
+ return null;
}
- result = { mediaSource, sessionId, url };
+ result = { mediaSource, sessionId, url, requiredHttpHeaders };
}
setStream(result);
setStreamStatus({ isLoading: false, isError: false });
+ return result;
} catch (error) {
console.error("Failed to fetch stream:", error);
setStreamStatus({ isLoading: false, isError: true });
+ return null;
}
};
+
+ // Store the fetch function in ref for use by refresh handler
+ refetchStreamRef.current = fetchStreamData;
fetchStreamData();
}, [
itemId,
@@ -408,7 +469,9 @@ export default function page() {
setIsPlaybackStopped(true);
videoRef.current?.pause();
revalidateProgressCache();
- }, [videoRef, reportPlaybackStopped, progress]);
+ // Resume inactivity timer when leaving player (TV only)
+ resumeInactivityTimer();
+ }, [videoRef, reportPlaybackStopped, progress, resumeInactivityTimer]);
useEffect(() => {
const beforeRemoveListener = navigation.addListener("beforeRemove", stop);
@@ -424,8 +487,11 @@ export default function page() {
return {
ItemId: item.Id,
- AudioStreamIndex: audioIndex,
- SubtitleStreamIndex: subtitleIndex,
+ // Report the live selection so server-side session/resume state reflects
+ // mid-playback track changes. Note: index 0 is valid (don't treat as
+ // falsy); -1 means "off" and is reported as-is.
+ AudioStreamIndex: currentAudioIndex,
+ SubtitleStreamIndex: currentSubtitleIndex,
MediaSourceId: mediaSourceId,
PositionTicks: msToTicks(progress.get()),
IsPaused: !isPlaying,
@@ -439,8 +505,8 @@ export default function page() {
}, [
stream,
item?.Id,
- audioIndex,
- subtitleIndex,
+ currentAudioIndex,
+ currentSubtitleIndex,
mediaSourceId,
progress,
isPlaying,
@@ -507,8 +573,8 @@ export default function page() {
},
[
item?.Id,
- audioIndex,
- subtitleIndex,
+ currentAudioIndex,
+ currentSubtitleIndex,
mediaSourceId,
isPlaying,
stream,
@@ -518,11 +584,6 @@ export default function page() {
],
);
- /** Gets the initial playback position in seconds. */
- const _startPosition = useMemo(() => {
- return ticksToSeconds(getInitialPlaybackTicks());
- }, [getInitialPlaybackTicks]);
-
/** Prepare metadata for iOS native media controls (Control Center, Lock Screen) */
const nowPlayingMetadata = useMemo(() => {
if (!item || !api) return undefined;
@@ -600,6 +661,15 @@ export default function page() {
autoplay: true,
initialSubtitleId,
initialAudioId,
+ // Pass cache/buffer settings from user preferences
+ cacheConfig: {
+ enabled: settings.mpvCacheEnabled,
+ cacheSeconds: settings.mpvCacheSeconds,
+ maxBytes: settings.mpvDemuxerMaxBytes,
+ maxBackBytes: settings.mpvDemuxerMaxBackBytes,
+ },
+ // Pass VO driver setting (Android only)
+ voDriver: settings.mpvVoDriver,
};
// Add external subtitles only for online playback
@@ -607,17 +677,32 @@ export default function page() {
source.externalSubtitles = externalSubs;
}
- // Add auth headers only for online streaming (not for local file:// URLs)
- if (!offline && api?.accessToken) {
- source.headers = {
- Authorization: `MediaBrowser Token="${api.accessToken}"`,
- };
+ // Add headers for online streaming (not for local file:// URLs)
+ if (!offline) {
+ const headers: Record = {};
+ const isRemoteStream =
+ mediaSource?.IsRemote && mediaSource?.Protocol === "Http";
+
+ // Add auth header only for Jellyfin API requests (not for external/remote streams)
+ if (api?.accessToken && !isRemoteStream) {
+ headers.Authorization = `MediaBrowser Token="${api.accessToken}"`;
+ }
+
+ // Add any required headers from the media source (e.g., for external/remote streams)
+ if (stream?.requiredHttpHeaders) {
+ Object.assign(headers, stream.requiredHttpHeaders);
+ }
+
+ if (Object.keys(headers).length > 0) {
+ source.headers = headers;
+ }
}
return source;
}, [
stream?.url,
stream?.mediaSource,
+ stream?.requiredHttpHeaders,
item?.UserData?.PlaybackPositionTicks,
playbackPositionFromUrl,
api?.basePath,
@@ -625,6 +710,11 @@ export default function page() {
subtitleIndex,
audioIndex,
offline,
+ settings.mpvCacheEnabled,
+ settings.mpvCacheSeconds,
+ settings.mpvDemuxerMaxBytes,
+ settings.mpvDemuxerMaxBackBytes,
+ settings.mpvVoDriver,
]);
const volumeUpCb = useCallback(async () => {
@@ -715,23 +805,27 @@ export default function page() {
setIsPlaying(true);
setIsBuffering(false);
setHasPlaybackStarted(true);
+ // Pause inactivity timer during playback (TV only)
+ pauseInactivityTimer();
if (item?.Id) {
playbackManager.reportPlaybackProgress(
currentPlayStateInfo() as PlaybackProgressInfo,
);
}
- if (!Platform.isTV) await activateKeepAwakeAsync();
+ await activateKeepAwakeAsync();
return;
}
if (isPaused) {
setIsPlaying(false);
+ // Resume inactivity timer when paused (TV only)
+ resumeInactivityTimer();
if (item?.Id) {
playbackManager.reportPlaybackProgress(
currentPlayStateInfo() as PlaybackProgressInfo,
);
}
- if (!Platform.isTV) await deactivateKeepAwake();
+ await deactivateKeepAwake();
return;
}
@@ -739,15 +833,19 @@ export default function page() {
setIsBuffering(isLoading);
}
},
- [playbackManager, item?.Id, progress],
+ [
+ playbackManager,
+ item?.Id,
+ progress,
+ pauseInactivityTimer,
+ resumeInactivityTimer,
+ ],
);
- /** PiP handler for MPV */
const _onPictureInPictureChange = useCallback(
(e: { nativeEvent: { isActive: boolean } }) => {
const { isActive } = e.nativeEvent;
setIsPipMode(isActive);
- // Hide controls when entering PiP
if (isActive) {
_setShowControls(false);
}
@@ -765,6 +863,9 @@ export default function page() {
// Memoize video ref functions to prevent unnecessary re-renders
const startPictureInPicture = useCallback(async () => {
+ // Hide controls BEFORE entering PiP so the window captures a clean view
+ _setShowControls(false);
+ setIsPipMode(true);
return videoRef.current?.startPictureInPicture?.();
}, []);
@@ -781,6 +882,55 @@ export default function page() {
videoRef.current?.seekTo?.(position / 1000);
}, []);
+ // TV audio track change handler
+ const handleAudioIndexChange = useCallback(
+ async (index: number) => {
+ setCurrentAudioIndex(index);
+
+ // Check if we're transcoding
+ const isTranscoding = Boolean(stream?.mediaSource?.TranscodingUrl);
+
+ // Convert Jellyfin index to MPV track ID
+ const mpvTrackId = getMpvAudioId(
+ stream?.mediaSource,
+ index,
+ isTranscoding,
+ );
+
+ if (mpvTrackId !== undefined) {
+ await videoRef.current?.setAudioTrack?.(mpvTrackId);
+ }
+ },
+ [stream?.mediaSource],
+ );
+
+ // TV subtitle track change handler
+ const handleSubtitleIndexChange = useCallback(
+ async (index: number) => {
+ setCurrentSubtitleIndex(index);
+
+ // Check if we're transcoding
+ const isTranscoding = Boolean(stream?.mediaSource?.TranscodingUrl);
+
+ if (index === -1) {
+ // Disable subtitles
+ await videoRef.current?.disableSubtitles?.();
+ } else {
+ // Convert Jellyfin index to MPV track ID
+ const mpvTrackId = getMpvSubtitleId(
+ stream?.mediaSource,
+ index,
+ isTranscoding,
+ );
+
+ if (mpvTrackId !== undefined && mpvTrackId !== -1) {
+ await videoRef.current?.setSubtitleTrack?.(mpvTrackId);
+ }
+ }
+ },
+ [stream?.mediaSource],
+ );
+
// Technical info toggle handler
const handleToggleTechnicalInfo = useCallback(() => {
setShowTechnicalInfo((prev) => !prev);
@@ -870,6 +1020,113 @@ export default function page() {
}
}, [isZoomedToFill, stream?.mediaSource, screenWidth, screenHeight]);
+ // TV: Navigate to previous item
+ const goToPreviousItem = useCallback(() => {
+ if (!previousItem || !settings) return;
+
+ const {
+ mediaSource: newMediaSource,
+ audioIndex: defaultAudioIndex,
+ subtitleIndex: defaultSubtitleIndex,
+ } = getDefaultPlaySettings(previousItem, settings, {
+ indexes: {
+ // Use the live selection, not the stale URL params (see goToNextItem).
+ subtitleIndex: currentSubtitleIndex,
+ audioIndex: currentAudioIndex,
+ },
+ source: stream?.mediaSource ?? undefined,
+ });
+
+ const queryParams = new URLSearchParams({
+ itemId: previousItem.Id ?? "",
+ audioIndex: defaultAudioIndex?.toString() ?? "",
+ subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
+ mediaSourceId: newMediaSource?.Id ?? "",
+ bitrateValue: bitrateValue?.toString() ?? "",
+ playbackPosition:
+ previousItem.UserData?.PlaybackPositionTicks?.toString() ?? "",
+ }).toString();
+
+ router.replace(`player/direct-player?${queryParams}` as any);
+ }, [
+ previousItem,
+ settings,
+ currentSubtitleIndex,
+ currentAudioIndex,
+ stream?.mediaSource,
+ bitrateValue,
+ router,
+ ]);
+
+ // TV: Add subtitle file to player (for client-side downloaded subtitles)
+ const addSubtitleFile = useCallback(async (path: string) => {
+ await videoRef.current?.addSubtitleFile?.(path, true);
+ }, []);
+
+ // TV: Refresh subtitle tracks after server-side subtitle download
+ // Re-fetches the media source to pick up newly downloaded subtitles
+ const handleRefreshSubtitleTracks = useCallback(async (): Promise<
+ MediaStream[]
+ > => {
+ if (!refetchStreamRef.current) return [];
+
+ const newStream = await refetchStreamRef.current();
+
+ // Check if component is still mounted before updating state
+ // This callback may be invoked from a modal after the player unmounts
+ if (!isMounted) return [];
+
+ if (newStream) {
+ setStream(newStream);
+ return (
+ newStream.mediaSource?.MediaStreams?.filter(
+ (s) => s.Type === "Subtitle",
+ ) ?? []
+ );
+ }
+ return [];
+ }, [isMounted]);
+
+ // TV: Navigate to next item
+ const goToNextItem = useCallback(() => {
+ if (!nextItem || !settings || isPlaybackStopped) return;
+
+ const {
+ mediaSource: newMediaSource,
+ audioIndex: defaultAudioIndex,
+ subtitleIndex: defaultSubtitleIndex,
+ } = getDefaultPlaySettings(nextItem, settings, {
+ indexes: {
+ // Use the live selection (updated when the user changes tracks
+ // mid-playback), not the stale URL params the episode started with.
+ subtitleIndex: currentSubtitleIndex,
+ audioIndex: currentAudioIndex,
+ },
+ source: stream?.mediaSource ?? undefined,
+ });
+
+ const queryParams = new URLSearchParams({
+ itemId: nextItem.Id ?? "",
+ audioIndex: defaultAudioIndex?.toString() ?? "",
+ subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
+ mediaSourceId: newMediaSource?.Id ?? "",
+ bitrateValue: bitrateValue?.toString() ?? "",
+ playbackPosition:
+ nextItem.UserData?.PlaybackPositionTicks?.toString() ?? "",
+ }).toString();
+
+ router.replace(`player/direct-player?${queryParams}` as any);
+ }, [
+ nextItem,
+ settings,
+ currentSubtitleIndex,
+ currentAudioIndex,
+ stream?.mediaSource,
+ bitrateValue,
+ router,
+ isPlaybackStopped,
+ ]);
+
// Apply subtitle settings when video loads
useEffect(() => {
if (!isVideoLoaded || !videoRef.current) return;
@@ -889,14 +1146,27 @@ export default function page() {
if (settings.mpvSubtitleAlignY !== undefined) {
await videoRef.current?.setSubtitleAlignY?.(settings.mpvSubtitleAlignY);
}
- if (settings.mpvSubtitleFontSize !== undefined) {
- await videoRef.current?.setSubtitleFontSize?.(
- settings.mpvSubtitleFontSize,
+ // Apply subtitle background (iOS only - doesn't work on tvOS due to composite OSD limitation)
+ // mpv uses #RRGGBBAA format (alpha last, same as CSS)
+ if (settings.mpvSubtitleBackgroundEnabled) {
+ const opacity = settings.mpvSubtitleBackgroundOpacity ?? 75;
+ const alphaHex = Math.round((opacity / 100) * 255)
+ .toString(16)
+ .padStart(2, "0")
+ .toUpperCase();
+ // Enable background-box mode (required for sub-back-color to work)
+ await videoRef.current?.setSubtitleBorderStyle?.("background-box");
+ await videoRef.current?.setSubtitleBackgroundColor?.(
+ `#000000${alphaHex}`,
);
- }
- // Apply subtitle size from general settings
- if (settings.subtitleSize) {
- await videoRef.current?.setSubtitleFontSize?.(settings.subtitleSize);
+ // Force override ASS subtitle styles so background shows on styled subtitles
+ await videoRef.current?.setSubtitleAssOverride?.("force");
+ } else {
+ // Restore default outline-and-shadow style
+ await videoRef.current?.setSubtitleBorderStyle?.("outline-and-shadow");
+ await videoRef.current?.setSubtitleBackgroundColor?.("#00000000");
+ // Restore default ASS behavior (keep original styles)
+ await videoRef.current?.setSubtitleAssOverride?.("no");
}
};
@@ -917,6 +1187,28 @@ export default function page() {
applyInitialPlaybackSpeed();
}, [isVideoLoaded, initialPlaybackSpeed]);
+ // TV only: Pre-load locally downloaded subtitles when video loads
+ // This adds them to MPV's track list without auto-selecting them
+ useEffect(() => {
+ if (!Platform.isTV || !isVideoLoaded || !videoRef.current || !itemId)
+ return;
+
+ const preloadLocalSubtitles = async () => {
+ const localSubs = getSubtitlesForItem(itemId);
+ for (const sub of localSubs) {
+ // Verify file still exists (cache may have been cleared)
+ const subtitleFile = new File(sub.filePath);
+ if (!subtitleFile.exists) {
+ continue;
+ }
+ // Add subtitle file to MPV without selecting it (select: false)
+ await videoRef.current?.addSubtitleFile?.(sub.filePath, false);
+ }
+ };
+
+ preloadLocalSubtitles();
+ }, [isVideoLoaded, itemId]);
+
// Show error UI first, before checking loading/missing‐data
if (itemStatus.isError || streamStatus.isError) {
return (
@@ -979,6 +1271,7 @@ export default function page() {
nowPlayingMetadata={nowPlayingMetadata}
onProgress={onProgress}
onPlaybackStateChange={onPlaybackStateChanged}
+ onPictureInPictureChange={_onPictureInPictureChange}
onLoad={() => setIsVideoLoaded(true)}
onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => {
console.error("Video Error:", e.nativeEvent);
@@ -1009,37 +1302,72 @@ export default function page() {
)}
- {isMounted === true && item && !isPipMode && (
-
- )}
+ {isMounted === true &&
+ item &&
+ !isPipMode &&
+ (Platform.isTV ? (
+
+ ) : (
+
+ ))}
diff --git a/app/(auth)/tv-option-modal.tsx b/app/(auth)/tv-option-modal.tsx
new file mode 100644
index 000000000..180228e36
--- /dev/null
+++ b/app/(auth)/tv-option-modal.tsx
@@ -0,0 +1,189 @@
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ Animated,
+ Easing,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { TVOptionCard } from "@/components/tv";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import { useTVBackPress } from "@/hooks/useTVBackPress";
+import { tvOptionModalAtom } from "@/utils/atoms/tvOptionModal";
+import { scaleSize } from "@/utils/scaleSize";
+import { store } from "@/utils/store";
+
+export default function TVOptionModal() {
+ const router = useRouter();
+ const modalState = useAtomValue(tvOptionModalAtom);
+ const typography = useScaledTVTypography();
+
+ const [isReady, setIsReady] = useState(false);
+ const firstCardRef = useRef(null);
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(200)).current;
+
+ const initialSelectedIndex = useMemo(() => {
+ if (!modalState?.options) return 0;
+ const idx = modalState.options.findIndex((o) => o.selected);
+ return idx >= 0 ? idx : 0;
+ }, [modalState?.options]);
+
+ // Animate in on mount and cleanup atom on unmount
+ useEffect(() => {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(200);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ // Delay focus setup to allow layout
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => {
+ clearTimeout(timer);
+ // Clear the atom on unmount to prevent stale callbacks from being retained
+ store.set(tvOptionModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ // Request focus on the first card when ready
+ useEffect(() => {
+ if (isReady && firstCardRef.current) {
+ const timer = setTimeout(() => {
+ (firstCardRef.current as any)?.requestTVFocus?.();
+ }, 50);
+ return () => clearTimeout(timer);
+ }
+ }, [isReady]);
+
+ const handleSelect = (value: any) => {
+ modalState?.onSelect(value);
+ store.set(tvOptionModalAtom, null);
+ router.back();
+ };
+
+ const handleClose = useCallback(() => {
+ store.set(tvOptionModalAtom, null);
+ router.back();
+ }, [router]);
+
+ // Intercept back/menu press to close the modal instead of the player
+ useTVBackPress(() => {
+ handleClose();
+ return true;
+ }, [handleClose]);
+
+ // If no modal state, just go back (shouldn't happen in normal usage)
+ if (!modalState) {
+ return null;
+ }
+
+ const { title, options } = modalState;
+ const scaledCardWidth = scaleSize(160);
+ const scaledCardHeight = scaleSize(75);
+
+ return (
+
+
+
+
+
+ {title}
+
+ {isReady && (
+
+ {options.map((option, index) => (
+ handleSelect(option.value)}
+ width={scaledCardWidth}
+ height={scaledCardHeight}
+ />
+ ))}
+
+ )}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ justifyContent: "flex-end",
+ },
+ sheetContainer: {
+ width: "100%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: scaleSize(24),
+ borderTopRightRadius: scaleSize(24),
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: scaleSize(24),
+ paddingBottom: scaleSize(50),
+ overflow: "visible",
+ },
+ title: {
+ fontWeight: "500",
+ color: "rgba(255,255,255,0.6)",
+ marginBottom: scaleSize(16),
+ paddingHorizontal: scaleSize(48),
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ },
+ scrollView: {
+ overflow: "visible",
+ },
+ scrollContent: {
+ paddingHorizontal: scaleSize(48),
+ paddingVertical: scaleSize(20),
+ gap: scaleSize(12),
+ },
+});
diff --git a/app/(auth)/tv-request-modal.tsx b/app/(auth)/tv-request-modal.tsx
new file mode 100644
index 000000000..372bd542f
--- /dev/null
+++ b/app/(auth)/tv-request-modal.tsx
@@ -0,0 +1,496 @@
+import { Ionicons } from "@expo/vector-icons";
+import { useQuery } from "@tanstack/react-query";
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Animated,
+ Easing,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { TVRequestOptionRow } from "@/components/jellyseerr/tv/TVRequestOptionRow";
+import { TVToggleOptionRow } from "@/components/jellyseerr/tv/TVToggleOptionRow";
+import { TVButton, TVOptionSelector } from "@/components/tv";
+import type { TVOptionItem } from "@/components/tv/TVOptionSelector";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import { useJellyseerr } from "@/hooks/useJellyseerr";
+import { tvRequestModalAtom } from "@/utils/atoms/tvRequestModal";
+import type {
+ QualityProfile,
+ RootFolder,
+ Tag,
+} from "@/utils/jellyseerr/server/api/servarr/base";
+import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
+import { store } from "@/utils/store";
+
+export default function TVRequestModalPage() {
+ const typography = useScaledTVTypography();
+ const router = useRouter();
+ const modalState = useAtomValue(tvRequestModalAtom);
+ const { t } = useTranslation();
+ const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
+
+ const [isReady, setIsReady] = useState(false);
+ const [requestOverrides, setRequestOverrides] = useState({
+ mediaId: modalState?.id ? Number(modalState.id) : 0,
+ mediaType: modalState?.mediaType,
+ userId: jellyseerrUser?.id,
+ });
+
+ const [activeSelector, setActiveSelector] = useState<
+ "profile" | "folder" | "user" | null
+ >(null);
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(200)).current;
+
+ // Animate in on mount
+ useEffect(() => {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(200);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => {
+ clearTimeout(timer);
+ store.set(tvRequestModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ const { data: serviceSettings } = useQuery({
+ queryKey: ["jellyseerr", "request", modalState?.mediaType, "service"],
+ queryFn: async () =>
+ jellyseerrApi?.service(
+ modalState?.mediaType === "movie" ? "radarr" : "sonarr",
+ ),
+ enabled: !!jellyseerrApi && !!jellyseerrUser && !!modalState,
+ });
+
+ const { data: users } = useQuery({
+ queryKey: ["jellyseerr", "users"],
+ queryFn: async () =>
+ jellyseerrApi?.user({ take: 1000, sort: "displayname" }),
+ enabled: !!jellyseerrApi && !!jellyseerrUser && !!modalState,
+ });
+
+ const defaultService = useMemo(
+ () => serviceSettings?.find?.((v) => v.isDefault),
+ [serviceSettings],
+ );
+
+ const { data: defaultServiceDetails } = useQuery({
+ queryKey: [
+ "jellyseerr",
+ "request",
+ modalState?.mediaType,
+ "service",
+ "details",
+ defaultService?.id,
+ ],
+ queryFn: async () => {
+ setRequestOverrides((prev) => ({
+ ...prev,
+ serverId: defaultService?.id,
+ }));
+ return jellyseerrApi?.serviceDetails(
+ modalState?.mediaType === "movie" ? "radarr" : "sonarr",
+ defaultService!.id,
+ );
+ },
+ enabled:
+ !!jellyseerrApi && !!jellyseerrUser && !!defaultService && !!modalState,
+ });
+
+ const defaultProfile: QualityProfile | undefined = useMemo(
+ () =>
+ defaultServiceDetails?.profiles.find(
+ (p) => p.id === defaultServiceDetails.server?.activeProfileId,
+ ),
+ [defaultServiceDetails],
+ );
+
+ const defaultFolder: RootFolder | undefined = useMemo(
+ () =>
+ defaultServiceDetails?.rootFolders.find(
+ (f) => f.path === defaultServiceDetails.server?.activeDirectory,
+ ),
+ [defaultServiceDetails],
+ );
+
+ const defaultTags: Tag[] = useMemo(() => {
+ return (
+ defaultServiceDetails?.tags.filter((t) =>
+ defaultServiceDetails?.server.activeTags?.includes(t.id),
+ ) ?? []
+ );
+ }, [defaultServiceDetails]);
+
+ const pathTitleExtractor = (item: RootFolder) =>
+ `${item.path} (${item.freeSpace.bytesToReadable()})`;
+
+ // Option builders
+ const qualityProfileOptions: TVOptionItem[] = useMemo(
+ () =>
+ defaultServiceDetails?.profiles.map((profile) => ({
+ label: profile.name,
+ value: profile.id,
+ selected:
+ (requestOverrides.profileId || defaultProfile?.id) === profile.id,
+ })) || [],
+ [
+ defaultServiceDetails?.profiles,
+ defaultProfile,
+ requestOverrides.profileId,
+ ],
+ );
+
+ const rootFolderOptions: TVOptionItem[] = useMemo(
+ () =>
+ defaultServiceDetails?.rootFolders.map((folder) => ({
+ label: pathTitleExtractor(folder),
+ value: folder.path,
+ selected:
+ (requestOverrides.rootFolder || defaultFolder?.path) === folder.path,
+ })) || [],
+ [
+ defaultServiceDetails?.rootFolders,
+ defaultFolder,
+ requestOverrides.rootFolder,
+ ],
+ );
+
+ const userOptions: TVOptionItem[] = useMemo(
+ () =>
+ users?.map((user) => ({
+ label: user.displayName,
+ value: user.id,
+ selected: (requestOverrides.userId || jellyseerrUser?.id) === user.id,
+ })) || [],
+ [users, jellyseerrUser, requestOverrides.userId],
+ );
+
+ const tagItems = useMemo(() => {
+ return (
+ defaultServiceDetails?.tags.map((tag) => ({
+ id: tag.id,
+ label: tag.label,
+ selected:
+ requestOverrides.tags?.includes(tag.id) ||
+ defaultTags.some((dt) => dt.id === tag.id),
+ })) ?? []
+ );
+ }, [defaultServiceDetails?.tags, defaultTags, requestOverrides.tags]);
+
+ // Selected display values
+ const selectedProfileName = useMemo(() => {
+ const profile = defaultServiceDetails?.profiles.find(
+ (p) => p.id === (requestOverrides.profileId || defaultProfile?.id),
+ );
+ return profile?.name || defaultProfile?.name || t("jellyseerr.select");
+ }, [
+ defaultServiceDetails?.profiles,
+ requestOverrides.profileId,
+ defaultProfile,
+ t,
+ ]);
+
+ const selectedFolderName = useMemo(() => {
+ const folder = defaultServiceDetails?.rootFolders.find(
+ (f) => f.path === (requestOverrides.rootFolder || defaultFolder?.path),
+ );
+ return folder
+ ? pathTitleExtractor(folder)
+ : defaultFolder
+ ? pathTitleExtractor(defaultFolder)
+ : t("jellyseerr.select");
+ }, [
+ defaultServiceDetails?.rootFolders,
+ requestOverrides.rootFolder,
+ defaultFolder,
+ t,
+ ]);
+
+ const selectedUserName = useMemo(() => {
+ const user = users?.find(
+ (u) => u.id === (requestOverrides.userId || jellyseerrUser?.id),
+ );
+ return (
+ user?.displayName || jellyseerrUser?.displayName || t("jellyseerr.select")
+ );
+ }, [users, requestOverrides.userId, jellyseerrUser, t]);
+
+ // Handlers
+ const handleProfileChange = useCallback((profileId: number) => {
+ setRequestOverrides((prev) => ({ ...prev, profileId }));
+ setActiveSelector(null);
+ }, []);
+
+ const handleFolderChange = useCallback((rootFolder: string) => {
+ setRequestOverrides((prev) => ({ ...prev, rootFolder }));
+ setActiveSelector(null);
+ }, []);
+
+ const handleUserChange = useCallback((userId: number) => {
+ setRequestOverrides((prev) => ({ ...prev, userId }));
+ setActiveSelector(null);
+ }, []);
+
+ const handleTagToggle = useCallback(
+ (tagId: number) => {
+ setRequestOverrides((prev) => {
+ const currentTags = prev.tags || defaultTags.map((t) => t.id);
+ const hasTag = currentTags.includes(tagId);
+ return {
+ ...prev,
+ tags: hasTag
+ ? currentTags.filter((id) => id !== tagId)
+ : [...currentTags, tagId],
+ };
+ });
+ },
+ [defaultTags],
+ );
+
+ const handleRequest = useCallback(() => {
+ if (!modalState) return;
+
+ const body = {
+ is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k,
+ profileId: defaultProfile?.id,
+ rootFolder: defaultFolder?.path,
+ tags: defaultTags.map((t) => t.id),
+ ...modalState.requestBody,
+ ...requestOverrides,
+ };
+
+ const seasonTitle =
+ modalState.requestBody?.seasons?.length === 1
+ ? t("jellyseerr.season_number", {
+ season_number: modalState.requestBody.seasons[0],
+ })
+ : modalState.requestBody?.seasons &&
+ modalState.requestBody.seasons.length > 1
+ ? t("jellyseerr.season_all")
+ : undefined;
+
+ requestMedia(
+ seasonTitle ? `${modalState.title}, ${seasonTitle}` : modalState.title,
+ body,
+ () => {
+ modalState.onRequested();
+ router.back();
+ },
+ );
+ }, [
+ modalState,
+ requestOverrides,
+ defaultProfile,
+ defaultFolder,
+ defaultTags,
+ defaultService,
+ defaultServiceDetails,
+ requestMedia,
+ router,
+ t,
+ ]);
+
+ if (!modalState) {
+ return null;
+ }
+
+ const isDataLoaded = defaultService && defaultServiceDetails && users;
+
+ return (
+
+
+
+
+
+ {t("jellyseerr.advanced")}
+
+
+ {modalState.title}
+
+
+ {isDataLoaded && isReady ? (
+
+
+ setActiveSelector("profile")}
+ hasTVPreferredFocus
+ />
+ setActiveSelector("folder")}
+ />
+ setActiveSelector("user")}
+ />
+
+ {tagItems.length > 0 && (
+
+ )}
+
+
+ ) : (
+
+ {t("common.loading")}
+
+ )}
+
+ {isReady && (
+
+
+
+
+ {t("jellyseerr.request_button")}
+
+
+
+ )}
+
+
+
+
+ {/* Sub-selectors */}
+ setActiveSelector(null)}
+ cancelLabel={t("jellyseerr.cancel")}
+ />
+ setActiveSelector(null)}
+ cancelLabel={t("jellyseerr.cancel")}
+ cardWidth={280}
+ />
+ setActiveSelector(null)}
+ cancelLabel={t("jellyseerr.cancel")}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ justifyContent: "flex-end",
+ },
+ sheetContainer: {
+ width: "100%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: 24,
+ paddingBottom: 50,
+ paddingHorizontal: 44,
+ overflow: "visible",
+ },
+ heading: {
+ fontWeight: "bold",
+ color: "#FFFFFF",
+ marginBottom: 8,
+ },
+ subtitle: {
+ color: "rgba(255,255,255,0.6)",
+ marginBottom: 24,
+ },
+ scrollView: {
+ maxHeight: 320,
+ overflow: "visible",
+ },
+ optionsContainer: {
+ gap: 12,
+ paddingVertical: 8,
+ paddingHorizontal: 4,
+ },
+ loadingContainer: {
+ height: 200,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ loadingText: {
+ color: "rgba(255,255,255,0.5)",
+ },
+ buttonContainer: {
+ marginTop: 24,
+ },
+ buttonText: {
+ fontWeight: "bold",
+ color: "#FFFFFF",
+ },
+});
diff --git a/app/(auth)/tv-season-select-modal.tsx b/app/(auth)/tv-season-select-modal.tsx
new file mode 100644
index 000000000..b9285e65f
--- /dev/null
+++ b/app/(auth)/tv-season-select-modal.tsx
@@ -0,0 +1,446 @@
+import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import { orderBy } from "lodash";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Animated,
+ Easing,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { TVButton } from "@/components/tv";
+import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import { useJellyseerr } from "@/hooks/useJellyseerr";
+import { useTVRequestModal } from "@/hooks/useTVRequestModal";
+import { tvSeasonSelectModalAtom } from "@/utils/atoms/tvSeasonSelectModal";
+import {
+ MediaStatus,
+ MediaType,
+} from "@/utils/jellyseerr/server/constants/media";
+import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
+import { store } from "@/utils/store";
+
+interface TVSeasonToggleCardProps {
+ season: {
+ id: number;
+ seasonNumber: number;
+ episodeCount: number;
+ status: MediaStatus;
+ };
+ selected: boolean;
+ onToggle: () => void;
+ canRequest: boolean;
+ hasTVPreferredFocus?: boolean;
+}
+
+const TVSeasonToggleCard: React.FC = ({
+ season,
+ selected,
+ onToggle,
+ canRequest,
+ hasTVPreferredFocus,
+}) => {
+ const { t } = useTranslation();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.08 });
+
+ // Get status icon and color based on MediaStatus
+ const getStatusIcon = (): {
+ icon: keyof typeof MaterialCommunityIcons.glyphMap;
+ color: string;
+ } | null => {
+ switch (season.status) {
+ case MediaStatus.PROCESSING:
+ return { icon: "clock", color: "#6366f1" };
+ case MediaStatus.AVAILABLE:
+ return { icon: "check", color: "#22c55e" };
+ case MediaStatus.PENDING:
+ return { icon: "bell", color: "#eab308" };
+ case MediaStatus.PARTIALLY_AVAILABLE:
+ return { icon: "minus", color: "#22c55e" };
+ case MediaStatus.BLACKLISTED:
+ return { icon: "eye-off", color: "#ef4444" };
+ default:
+ return canRequest ? { icon: "plus", color: "#22c55e" } : null;
+ }
+ };
+
+ const statusInfo = getStatusIcon();
+ const isDisabled = !canRequest;
+
+ return (
+
+
+ {/* Checkmark for selected */}
+
+ {selected && (
+
+ )}
+
+
+ {/* Season info */}
+
+
+ {t("jellyseerr.season_number", {
+ season_number: season.seasonNumber,
+ })}
+
+
+
+ {t("jellyseerr.number_episodes", {
+ episode_number: season.episodeCount,
+ })}
+
+ {statusInfo && (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default function TVSeasonSelectModalPage() {
+ const typography = useScaledTVTypography();
+ const router = useRouter();
+ const modalState = useAtomValue(tvSeasonSelectModalAtom);
+ const { t } = useTranslation();
+ const { requestMedia } = useJellyseerr();
+ const { showRequestModal } = useTVRequestModal();
+
+ // Selected seasons - initially select all requestable (UNKNOWN status) seasons
+ const [selectedSeasons, setSelectedSeasons] = useState>(
+ new Set(),
+ );
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(200)).current;
+
+ // Initialize selected seasons when modal state changes
+ useEffect(() => {
+ if (modalState?.seasons) {
+ const requestableSeasons = modalState.seasons
+ .filter((s) => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0)
+ .map((s) => s.seasonNumber);
+ setSelectedSeasons(new Set(requestableSeasons));
+ }
+ }, [modalState?.seasons]);
+
+ // Animate in on mount
+ useEffect(() => {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(200);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ return () => {
+ store.set(tvSeasonSelectModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ // Sort seasons by season number (ascending)
+ const sortedSeasons = useMemo(() => {
+ if (!modalState?.seasons) return [];
+ return orderBy(
+ modalState.seasons.filter((s) => s.seasonNumber !== 0),
+ "seasonNumber",
+ "asc",
+ );
+ }, [modalState?.seasons]);
+
+ // Find the index of the first requestable season for initial focus
+ const firstRequestableIndex = useMemo(() => {
+ return sortedSeasons.findIndex((s) => s.status === MediaStatus.UNKNOWN);
+ }, [sortedSeasons]);
+
+ const handleToggleSeason = useCallback((seasonNumber: number) => {
+ setSelectedSeasons((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(seasonNumber)) {
+ newSet.delete(seasonNumber);
+ } else {
+ newSet.add(seasonNumber);
+ }
+ return newSet;
+ });
+ }, []);
+
+ const handleRequestSelected = useCallback(() => {
+ if (!modalState || selectedSeasons.size === 0) return;
+
+ const seasonsArray = Array.from(selectedSeasons);
+ const body: MediaRequestBody = {
+ mediaId: modalState.mediaId,
+ mediaType: MediaType.TV,
+ tvdbId: modalState.tvdbId,
+ seasons: seasonsArray,
+ };
+
+ if (modalState.hasAdvancedRequestPermission) {
+ // Close this modal and open the advanced request modal
+ router.back();
+ showRequestModal({
+ requestBody: body,
+ title: modalState.title,
+ id: modalState.mediaId,
+ mediaType: MediaType.TV,
+ onRequested: modalState.onRequested,
+ });
+ return;
+ }
+
+ // Build the title based on selected seasons
+ const seasonTitle =
+ seasonsArray.length === 1
+ ? t("jellyseerr.season_number", { season_number: seasonsArray[0] })
+ : seasonsArray.length === sortedSeasons.length
+ ? t("jellyseerr.season_all")
+ : t("jellyseerr.n_selected", { count: seasonsArray.length });
+
+ requestMedia(`${modalState.title}, ${seasonTitle}`, body, () => {
+ modalState.onRequested();
+ router.back();
+ });
+ }, [
+ modalState,
+ selectedSeasons,
+ sortedSeasons.length,
+ requestMedia,
+ router,
+ t,
+ showRequestModal,
+ ]);
+
+ if (!modalState) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {t("jellyseerr.select_seasons")}
+
+
+ {modalState.title}
+
+
+ {/* Season cards horizontal scroll */}
+
+ {sortedSeasons.map((season, index) => {
+ const canRequestSeason = season.status === MediaStatus.UNKNOWN;
+ return (
+ handleToggleSeason(season.seasonNumber)}
+ canRequest={canRequestSeason}
+ hasTVPreferredFocus={index === firstRequestableIndex}
+ />
+ );
+ })}
+
+
+ {/* Request button */}
+
+
+
+
+ {t("jellyseerr.request_selected")}
+ {selectedSeasons.size > 0 && ` (${selectedSeasons.size})`}
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ justifyContent: "flex-end",
+ },
+ sheetContainer: {
+ width: "100%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: 24,
+ paddingBottom: 50,
+ paddingHorizontal: 44,
+ overflow: "visible",
+ },
+ heading: {
+ fontWeight: "bold",
+ color: "#FFFFFF",
+ marginBottom: 8,
+ },
+ subtitle: {
+ color: "rgba(255,255,255,0.6)",
+ marginBottom: 24,
+ },
+ scrollView: {
+ overflow: "visible",
+ },
+ scrollContent: {
+ paddingVertical: 12,
+ paddingHorizontal: 4,
+ gap: 16,
+ },
+ seasonCard: {
+ width: 160,
+ paddingVertical: 16,
+ paddingHorizontal: 16,
+ borderRadius: 12,
+ shadowColor: "#fff",
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.2,
+ shadowRadius: 8,
+ },
+ checkmarkContainer: {
+ height: 24,
+ marginBottom: 8,
+ },
+ seasonInfo: {
+ flex: 1,
+ },
+ seasonTitle: {
+ fontWeight: "600",
+ marginBottom: 4,
+ },
+ episodeRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+ episodeCount: {
+ fontSize: 14,
+ },
+ statusBadge: {
+ width: 22,
+ height: 22,
+ borderRadius: 11,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ buttonContainer: {
+ marginTop: 24,
+ },
+ buttonText: {
+ fontWeight: "bold",
+ color: "#FFFFFF",
+ },
+});
diff --git a/app/(auth)/tv-series-season-modal.tsx b/app/(auth)/tv-series-season-modal.tsx
new file mode 100644
index 000000000..b1117e6f4
--- /dev/null
+++ b/app/(auth)/tv-series-season-modal.tsx
@@ -0,0 +1,190 @@
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import { useEffect, useMemo, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Animated,
+ Easing,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { TVCancelButton, TVOptionCard } from "@/components/tv";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import { tvSeriesSeasonModalAtom } from "@/utils/atoms/tvSeriesSeasonModal";
+import { store } from "@/utils/store";
+
+export default function TVSeriesSeasonModalPage() {
+ const typography = useScaledTVTypography();
+ const router = useRouter();
+ const modalState = useAtomValue(tvSeriesSeasonModalAtom);
+ const { t } = useTranslation();
+
+ const [isReady, setIsReady] = useState(false);
+ const firstCardRef = useRef(null);
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(200)).current;
+
+ const initialSelectedIndex = useMemo(() => {
+ if (!modalState?.seasons) return 0;
+ const idx = modalState.seasons.findIndex((o) => o.selected);
+ return idx >= 0 ? idx : 0;
+ }, [modalState?.seasons]);
+
+ // Animate in on mount
+ useEffect(() => {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(200);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => {
+ clearTimeout(timer);
+ store.set(tvSeriesSeasonModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ // Focus on the selected card when ready
+ useEffect(() => {
+ if (isReady && firstCardRef.current) {
+ const timer = setTimeout(() => {
+ (firstCardRef.current as any)?.requestTVFocus?.();
+ }, 50);
+ return () => clearTimeout(timer);
+ }
+ }, [isReady]);
+
+ const handleSelect = (seasonIndex: number) => {
+ if (modalState?.onSeasonSelect) {
+ modalState.onSeasonSelect(seasonIndex);
+ }
+ router.back();
+ };
+
+ const handleCancel = () => {
+ router.back();
+ };
+
+ if (!modalState) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {t("item_card.select_season")}
+
+
+ {isReady && (
+
+ {modalState.seasons.map((season, index) => (
+ handleSelect(season.value)}
+ width={180}
+ height={85}
+ />
+ ))}
+
+ )}
+
+ {isReady && (
+
+
+
+ )}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ justifyContent: "flex-end",
+ },
+ sheetContainer: {
+ width: "100%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: 24,
+ paddingBottom: 50,
+ overflow: "visible",
+ },
+ title: {
+ fontWeight: "500",
+ color: "rgba(255,255,255,0.6)",
+ marginBottom: 16,
+ paddingHorizontal: 48,
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ },
+ scrollView: {
+ overflow: "visible",
+ },
+ scrollContent: {
+ paddingHorizontal: 48,
+ paddingVertical: 20,
+ gap: 12,
+ },
+ cancelButtonContainer: {
+ marginTop: 16,
+ paddingHorizontal: 48,
+ alignItems: "flex-start",
+ },
+});
diff --git a/app/(auth)/tv-subtitle-modal.tsx b/app/(auth)/tv-subtitle-modal.tsx
new file mode 100644
index 000000000..ed5d24d9c
--- /dev/null
+++ b/app/(auth)/tv-subtitle-modal.tsx
@@ -0,0 +1,1350 @@
+import { Ionicons } from "@expo/vector-icons";
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { useTranslation } from "react-i18next";
+import {
+ ActivityIndicator,
+ Animated,
+ Easing,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { TVTabButton, useTVFocusAnimation } from "@/components/tv";
+import type { Track } from "@/components/video-player/controls/types";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import {
+ type SubtitleSearchResult,
+ useRemoteSubtitles,
+} from "@/hooks/useRemoteSubtitles";
+import { useTVBackPress } from "@/hooks/useTVBackPress";
+import { useSettings } from "@/utils/atoms/settings";
+import { tvSubtitleModalAtom } from "@/utils/atoms/tvSubtitleModal";
+import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api";
+import { scaleSize } from "@/utils/scaleSize";
+import { store } from "@/utils/store";
+
+type TabType = "tracks" | "download" | "settings";
+
+// Track card for subtitle track selection
+const TVTrackCard = React.forwardRef<
+ View,
+ {
+ label: string;
+ sublabel?: string;
+ selected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+ }
+>(({ label, sublabel, selected, hasTVPreferredFocus, onPress }, ref) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05 });
+
+ return (
+
+
+
+ {label}
+
+ {sublabel && (
+
+ {sublabel}
+
+ )}
+ {selected && !focused && (
+
+
+
+ )}
+
+
+ );
+});
+
+// Language selector card
+const LanguageCard = React.forwardRef<
+ View,
+ {
+ code: string;
+ name: string;
+ selected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+ }
+>(({ code, name, selected, hasTVPreferredFocus, onPress }, ref) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05 });
+
+ return (
+
+
+
+ {name}
+
+
+ {code.toUpperCase()}
+
+ {selected && !focused && (
+
+
+
+ )}
+
+
+ );
+});
+
+// Subtitle result card
+const SubtitleResultCard = React.forwardRef<
+ View,
+ {
+ result: SubtitleSearchResult;
+ hasTVPreferredFocus?: boolean;
+ isDownloading?: boolean;
+ onPress: () => void;
+ }
+>(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.03 });
+
+ return (
+
+
+ {/* Provider/Source badge */}
+
+
+ {result.providerName}
+
+
+
+ {/* Name */}
+
+ {result.name}
+
+
+ {/* Meta info row */}
+
+ {/* Format */}
+
+ {result.format?.toUpperCase()}
+
+
+ {/* Rating if available */}
+ {result.communityRating !== undefined &&
+ result.communityRating > 0 && (
+
+
+
+ {result.communityRating.toFixed(1)}
+
+
+ )}
+
+ {/* Download count if available */}
+ {result.downloadCount !== undefined && result.downloadCount > 0 && (
+
+
+
+ {result.downloadCount.toLocaleString()}
+
+
+ )}
+
+
+ {/* Flags */}
+
+ {result.isHashMatch && (
+
+
+ Hash Match
+
+
+ )}
+ {result.hearingImpaired && (
+
+
+
+ )}
+ {result.aiTranslated && (
+
+
+ AI
+
+
+ )}
+
+
+ {/* Loading indicator when downloading */}
+ {isDownloading && (
+
+
+
+ )}
+
+
+ );
+});
+
+// Stepper button for subtitle size control
+const TVStepperButton: React.FC<{
+ icon: "remove" | "add";
+ onPress: () => void;
+ disabled?: boolean;
+ hasTVPreferredFocus?: boolean;
+}> = ({ icon, onPress, disabled, hasTVPreferredFocus }) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.1 });
+
+ return (
+
+
+
+
+
+ );
+};
+
+// Generic stepper control component
+const TVStepperControl: React.FC<{
+ value: number;
+ min: number;
+ max: number;
+ step: number;
+ formatValue: (value: number) => string;
+ onChange: (newValue: number) => void;
+ hasTVPreferredFocus?: boolean;
+}> = ({
+ value,
+ min,
+ max,
+ step,
+ formatValue,
+ onChange,
+ hasTVPreferredFocus,
+}) => {
+ const canDecrease = value > min;
+ const canIncrease = value < max;
+
+ const handleDecrease = () => {
+ if (canDecrease) {
+ const newValue = Math.max(min, Math.round((value - step) * 10) / 10);
+ onChange(newValue);
+ }
+ };
+
+ const handleIncrease = () => {
+ if (canIncrease) {
+ const newValue = Math.min(max, Math.round((value + step) * 10) / 10);
+ onChange(newValue);
+ }
+ };
+
+ return (
+
+
+
+ {formatValue(value)}
+
+
+
+ );
+};
+
+// Alignment option card
+const TVAlignmentCard: React.FC<{
+ label: string;
+ selected: boolean;
+ onPress: () => void;
+ hasTVPreferredFocus?: boolean;
+}> = ({ label, selected, onPress, hasTVPreferredFocus }) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05 });
+
+ return (
+
+
+
+ {label}
+
+ {selected && !focused && (
+
+
+
+ )}
+
+
+ );
+};
+
+export default function TVSubtitleModal() {
+ const router = useRouter();
+ const { t } = useTranslation();
+ const modalState = useAtomValue(tvSubtitleModalAtom);
+ const { settings, updateSettings } = useSettings();
+ const typography = useScaledTVTypography();
+
+ const [activeTab, setActiveTab] = useState("tracks");
+ const [selectedLanguage, setSelectedLanguage] = useState("eng");
+ const [downloadingId, setDownloadingId] = useState(null);
+ const [hasSearchedThisSession, setHasSearchedThisSession] = useState(false);
+ const [isReady, setIsReady] = useState(false);
+ const [isTabContentReady, setIsTabContentReady] = useState(false);
+ const firstTrackRef = useRef(null);
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(300)).current;
+
+ const {
+ hasOpenSubtitlesApiKey,
+ isSearching,
+ searchError,
+ searchResults,
+ search,
+ downloadAsync,
+ reset,
+ } = useRemoteSubtitles({
+ itemId: modalState?.item?.Id ?? "",
+ item: modalState?.item ?? ({} as any),
+ mediaSourceId: modalState?.mediaSourceId,
+ });
+
+ const resetRef = useRef(reset);
+ resetRef.current = reset;
+
+ const subtitleTracks = modalState?.subtitleTracks ?? [];
+ const currentSubtitleIndex = modalState?.currentSubtitleIndex ?? -1;
+
+ const initialSelectedTrackIndex = useMemo(() => {
+ if (currentSubtitleIndex === -1) return 0;
+ const trackIdx = subtitleTracks.findIndex(
+ (t) => t.index === currentSubtitleIndex,
+ );
+ return trackIdx >= 0 ? trackIdx + 1 : 0;
+ }, [subtitleTracks, currentSubtitleIndex]);
+
+ // Track if component is mounted for async operations
+ const isMountedRef = useRef(true);
+
+ // Animate in on mount and cleanup atom on unmount
+ useEffect(() => {
+ isMountedRef.current = true;
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(300);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => {
+ clearTimeout(timer);
+ isMountedRef.current = false;
+ // Clear the atom on unmount to prevent stale callbacks from being retained
+ store.set(tvSubtitleModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ useEffect(() => {
+ if (activeTab === "download" && !hasSearchedThisSession && modalState) {
+ search({ language: selectedLanguage });
+ setHasSearchedThisSession(true);
+ }
+ }, [activeTab, hasSearchedThisSession, search, selectedLanguage, modalState]);
+
+ useEffect(() => {
+ if (isReady) {
+ setIsTabContentReady(false);
+ const timer = setTimeout(() => setIsTabContentReady(true), 50);
+ return () => clearTimeout(timer);
+ }
+ setIsTabContentReady(false);
+ }, [activeTab, isReady]);
+
+ const handleClose = useCallback(() => {
+ store.set(tvSubtitleModalAtom, null);
+ router.back();
+ }, [router]);
+
+ // Intercept back/menu press to close the modal instead of the player
+ useTVBackPress(() => {
+ handleClose();
+ return true;
+ }, [handleClose]);
+
+ const handleLanguageSelect = useCallback(
+ (code: string) => {
+ setSelectedLanguage(code);
+ search({ language: code });
+ },
+ [search],
+ );
+
+ const handleTrackSelect = useCallback(
+ (option: { setTrack?: () => void }) => {
+ option.setTrack?.();
+ handleClose();
+ },
+ [handleClose],
+ );
+
+ const handleDownload = useCallback(
+ async (result: SubtitleSearchResult) => {
+ setDownloadingId(result.id);
+
+ try {
+ const downloadResult = await downloadAsync(result);
+
+ // Check if component is still mounted after async operation
+ if (!isMountedRef.current) return;
+
+ if (downloadResult.type === "server") {
+ // Give Jellyfin time to process the downloaded subtitle
+ await new Promise((resolve) => setTimeout(resolve, 5000));
+
+ // Check if component is still mounted after the wait
+ if (!isMountedRef.current) return;
+
+ // Refresh tracks and stay open for server-side downloads
+ if (modalState?.refreshSubtitleTracks) {
+ const newTracks = await modalState.refreshSubtitleTracks();
+
+ // Check if component is still mounted after fetching tracks
+ if (!isMountedRef.current) return;
+
+ // Update atom with new tracks
+ store.set(tvSubtitleModalAtom, {
+ ...modalState,
+ subtitleTracks: newTracks,
+ });
+ // Switch to tracks tab to show the new subtitle
+ setActiveTab("tracks");
+ }
+
+ // Also call onServerSubtitleDownloaded to invalidate React Query cache
+ // (used when opening modal from item detail page)
+ modalState?.onServerSubtitleDownloaded?.();
+
+ // Do NOT close modal - user can see and select the new track
+ } else if (downloadResult.type === "local" && downloadResult.path) {
+ // Notify parent that a local subtitle was downloaded
+ modalState?.onLocalSubtitleDownloaded?.(downloadResult.path);
+
+ // Check if component is still mounted after callback
+ if (!isMountedRef.current) return;
+
+ // Refresh tracks to include the newly downloaded subtitle
+ if (modalState?.refreshSubtitleTracks) {
+ const newTracks = await modalState.refreshSubtitleTracks();
+
+ // Check if component is still mounted after fetching tracks
+ if (!isMountedRef.current) return;
+
+ // Update atom with new tracks
+ store.set(tvSubtitleModalAtom, {
+ ...modalState,
+ subtitleTracks: newTracks,
+ });
+ // Switch to tracks tab to show the new subtitle
+ setActiveTab("tracks");
+ } else {
+ // No refreshSubtitleTracks available (e.g., from player), just close
+ handleClose();
+ }
+ }
+ } catch (error) {
+ console.error("Failed to download subtitle:", error);
+ } finally {
+ if (isMountedRef.current) {
+ setDownloadingId(null);
+ }
+ }
+ },
+ [downloadAsync, modalState, handleClose],
+ );
+
+ const displayLanguages = useMemo(
+ () => COMMON_SUBTITLE_LANGUAGES.slice(0, 16),
+ [],
+ );
+
+ const trackOptions = useMemo(() => {
+ const noneOption = {
+ label: t("item_card.subtitles.none"),
+ sublabel: undefined as string | undefined,
+ value: -1,
+ selected: currentSubtitleIndex === -1,
+ setTrack: () => modalState?.onDisableSubtitles?.(),
+ isLocal: false,
+ };
+ const options = subtitleTracks.map((track: Track) => ({
+ label: track.name,
+ sublabel: track.isLocal
+ ? t("player.downloaded") || "Downloaded"
+ : (undefined as string | undefined),
+ value: track.index,
+ selected: track.index === currentSubtitleIndex,
+ setTrack: track.setTrack,
+ isLocal: track.isLocal ?? false,
+ }));
+ return [noneOption, ...options];
+ }, [subtitleTracks, currentSubtitleIndex, t, modalState]);
+
+ if (!modalState) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {/* Header with tabs */}
+
+
+ {t("item_card.subtitles.label") || "Subtitles"}
+
+
+ {/* Tab bar */}
+
+ setActiveTab("tracks")}
+ />
+ setActiveTab("download")}
+ />
+ setActiveTab("settings")}
+ />
+
+
+
+ {/* Tracks Tab Content */}
+ {activeTab === "tracks" && isTabContentReady && (
+
+
+ {trackOptions.map((option, index) => (
+ handleTrackSelect(option)}
+ />
+ ))}
+
+
+ )}
+
+ {/* Download Tab Content */}
+ {activeTab === "download" && isTabContentReady && (
+ <>
+ {/* Language Selector */}
+
+
+ {t("player.language") || "Language"}
+
+
+ {displayLanguages.map((lang, index) => (
+ handleLanguageSelect(lang.code)}
+ />
+ ))}
+
+
+
+ {/* Results Section */}
+
+
+ {t("player.results") || "Results"}
+ {searchResults && ` (${searchResults.length})`}
+
+
+ {/* Loading state */}
+ {isSearching && (
+
+
+
+ )}
+
+ {/* Error state */}
+ {searchError && !isSearching && (
+
+
+
+ {t("player.search_failed") || "Search failed"}
+
+
+ {!hasOpenSubtitlesApiKey
+ ? t("player.no_subtitle_provider") ||
+ "No subtitle provider configured on server"
+ : String(searchError)}
+
+
+ )}
+
+ {/* No results */}
+ {searchResults &&
+ searchResults.length === 0 &&
+ !isSearching &&
+ !searchError && (
+
+
+
+ {t("player.no_subtitles_found") ||
+ "No subtitles found"}
+
+
+ )}
+
+ {/* Results list */}
+ {searchResults &&
+ searchResults.length > 0 &&
+ !isSearching && (
+
+ {searchResults.map((result, index) => (
+ handleDownload(result)}
+ />
+ ))}
+
+ )}
+
+
+ {/* API Key hint if no fallback available */}
+ {!hasOpenSubtitlesApiKey && (
+
+
+
+ {t("player.add_opensubtitles_key_hint") ||
+ "Add OpenSubtitles API key in settings for client-side fallback"}
+
+
+ )}
+ >
+ )}
+
+ {/* Settings Tab Content */}
+ {activeTab === "settings" && isTabContentReady && (
+
+
+ {/* Subtitle Scale */}
+
+ `${v.toFixed(1)}x`}
+ onChange={(newValue) => {
+ updateSettings({
+ mpvSubtitleScale: Math.round(newValue * 10) / 10,
+ });
+ }}
+ hasTVPreferredFocus={true}
+ />
+
+ {t("home.settings.subtitles.mpv_subtitle_scale") ||
+ "Subtitle Scale"}
+
+
+
+ {/* Vertical Margin */}
+
+ `${v}`}
+ onChange={(newValue) => {
+ updateSettings({ mpvSubtitleMarginY: newValue });
+ }}
+ />
+
+ {t("home.settings.subtitles.mpv_subtitle_margin_y") ||
+ "Vertical Margin"}
+
+
+
+ {/* Horizontal Alignment */}
+
+
+ {(["left", "center", "right"] as const).map((align) => (
+
+ updateSettings({ mpvSubtitleAlignX: align })
+ }
+ />
+ ))}
+
+
+ {t("home.settings.subtitles.mpv_subtitle_align_x") ||
+ "Horizontal Align"}
+
+
+
+ {/* Vertical Alignment */}
+
+
+ {(["top", "center", "bottom"] as const).map((align) => (
+
+ updateSettings({ mpvSubtitleAlignY: align })
+ }
+ />
+ ))}
+
+
+ {t("home.settings.subtitles.mpv_subtitle_align_y") ||
+ "Vertical Align"}
+
+
+
+
+ )}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
+ justifyContent: "flex-end",
+ },
+ sheetContainer: {
+ maxHeight: "70%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: scaleSize(24),
+ borderTopRightRadius: scaleSize(24),
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: scaleSize(24),
+ paddingBottom: scaleSize(48),
+ },
+ header: {
+ paddingHorizontal: scaleSize(48),
+ marginBottom: scaleSize(20),
+ },
+ title: {
+ fontWeight: "600",
+ color: "#fff",
+ marginBottom: scaleSize(16),
+ },
+ tabRow: {
+ flexDirection: "row",
+ gap: scaleSize(24),
+ },
+ section: {
+ marginBottom: scaleSize(20),
+ },
+ sectionTitle: {
+ fontWeight: "500",
+ color: "rgba(255,255,255,0.5)",
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ marginBottom: scaleSize(12),
+ paddingHorizontal: scaleSize(48),
+ },
+ tracksScroll: {
+ overflow: "visible",
+ },
+ tracksScrollContent: {
+ paddingHorizontal: scaleSize(48),
+ paddingVertical: scaleSize(8),
+ gap: scaleSize(12),
+ },
+ trackCard: {
+ width: scaleSize(180),
+ height: scaleSize(80),
+ borderRadius: scaleSize(14),
+ justifyContent: "center",
+ alignItems: "center",
+ paddingHorizontal: scaleSize(12),
+ },
+ trackCardText: {
+ textAlign: "center",
+ },
+ trackCardSublabel: {
+ marginTop: scaleSize(2),
+ },
+ checkmark: {
+ position: "absolute",
+ top: scaleSize(8),
+ right: scaleSize(8),
+ },
+ languageScroll: {
+ overflow: "visible",
+ },
+ languageScrollContent: {
+ paddingHorizontal: scaleSize(48),
+ paddingVertical: scaleSize(8),
+ gap: scaleSize(10),
+ },
+ languageCard: {
+ width: scaleSize(120),
+ height: scaleSize(60),
+ borderRadius: scaleSize(12),
+ justifyContent: "center",
+ alignItems: "center",
+ paddingHorizontal: scaleSize(12),
+ },
+ languageCardText: {
+ fontWeight: "500",
+ },
+ languageCardCode: {
+ marginTop: scaleSize(2),
+ },
+ resultsScroll: {
+ overflow: "visible",
+ },
+ resultsScrollContent: {
+ paddingHorizontal: scaleSize(48),
+ paddingVertical: scaleSize(8),
+ gap: scaleSize(12),
+ },
+ resultCard: {
+ width: scaleSize(220),
+ height: scaleSize(130),
+ borderRadius: scaleSize(14),
+ padding: scaleSize(14),
+ borderWidth: 1,
+ overflow: "hidden",
+ },
+ providerBadge: {
+ alignSelf: "flex-start",
+ paddingHorizontal: scaleSize(8),
+ paddingVertical: scaleSize(3),
+ borderRadius: scaleSize(6),
+ marginBottom: scaleSize(8),
+ },
+ providerText: {
+ fontWeight: "600",
+ textTransform: "uppercase",
+ letterSpacing: 0.5,
+ },
+ resultName: {
+ fontWeight: "500",
+ marginBottom: scaleSize(8),
+ lineHeight: scaleSize(18),
+ },
+ resultMeta: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(12),
+ marginBottom: scaleSize(8),
+ },
+ resultMetaText: {},
+ ratingContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(3),
+ },
+ downloadCountContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(3),
+ },
+ flagsContainer: {
+ flexDirection: "row",
+ gap: scaleSize(6),
+ flexWrap: "wrap",
+ },
+ flag: {
+ paddingHorizontal: scaleSize(6),
+ paddingVertical: scaleSize(2),
+ borderRadius: scaleSize(4),
+ },
+ flagText: {
+ fontWeight: "600",
+ color: "#fff",
+ },
+ downloadingOverlay: {
+ ...StyleSheet.absoluteFill,
+ backgroundColor: "rgba(0,0,0,0.5)",
+ borderRadius: scaleSize(14),
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ loadingContainer: {
+ paddingVertical: scaleSize(20),
+ alignItems: "center",
+ },
+ errorContainer: {
+ paddingVertical: scaleSize(40),
+ paddingHorizontal: scaleSize(48),
+ alignItems: "center",
+ },
+ errorText: {
+ color: "rgba(255,100,100,0.9)",
+ marginTop: scaleSize(8),
+ fontWeight: "500",
+ },
+ errorHint: {
+ color: "rgba(255,255,255,0.5)",
+ marginTop: scaleSize(4),
+ textAlign: "center",
+ },
+ emptyContainer: {
+ paddingVertical: scaleSize(40),
+ alignItems: "center",
+ },
+ emptyText: {
+ color: "rgba(255,255,255,0.5)",
+ marginTop: scaleSize(8),
+ },
+ apiKeyHint: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(8),
+ paddingHorizontal: scaleSize(48),
+ paddingTop: scaleSize(8),
+ },
+ apiKeyHintText: {},
+ // Settings tab styles
+ settingsScroll: {
+ maxHeight: scaleSize(300),
+ },
+ settingsScrollContent: {
+ paddingHorizontal: scaleSize(48),
+ paddingVertical: scaleSize(8),
+ gap: scaleSize(24),
+ },
+ settingRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+ settingLabel: {
+ fontWeight: "500",
+ color: "#fff",
+ },
+ sizeControlContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(16),
+ },
+ stepperButton: {
+ width: scaleSize(56),
+ height: scaleSize(56),
+ borderRadius: scaleSize(14),
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ sizeValueContainer: {
+ width: scaleSize(80),
+ alignItems: "center",
+ },
+ sizeValueText: {
+ fontWeight: "600",
+ color: "#fff",
+ fontSize: scaleSize(24),
+ },
+ alignmentRow: {
+ flexDirection: "row",
+ gap: scaleSize(10),
+ },
+ alignmentCard: {
+ paddingHorizontal: scaleSize(20),
+ paddingVertical: scaleSize(14),
+ borderRadius: scaleSize(12),
+ minWidth: scaleSize(90),
+ alignItems: "center",
+ },
+ alignmentCardText: {
+ textTransform: "capitalize",
+ },
+ alignmentCheckmark: {
+ position: "absolute",
+ top: scaleSize(6),
+ right: scaleSize(6),
+ },
+});
diff --git a/app/(auth)/tv-user-switch-modal.tsx b/app/(auth)/tv-user-switch-modal.tsx
new file mode 100644
index 000000000..1478b0f7b
--- /dev/null
+++ b/app/(auth)/tv-user-switch-modal.tsx
@@ -0,0 +1,174 @@
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Animated,
+ Easing,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { TVUserCard } from "@/components/tv/TVUserCard";
+import useRouter from "@/hooks/useAppRouter";
+import { tvUserSwitchModalAtom } from "@/utils/atoms/tvUserSwitchModal";
+import type { SavedServerAccount } from "@/utils/secureCredentials";
+import { store } from "@/utils/store";
+
+export default function TVUserSwitchModalPage() {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const modalState = useAtomValue(tvUserSwitchModalAtom);
+
+ const [isReady, setIsReady] = useState(false);
+ const firstCardRef = useRef(null);
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(200)).current;
+
+ // Animate in on mount and cleanup atom on unmount
+ useEffect(() => {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(200);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ // Delay focus setup to allow layout
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => {
+ clearTimeout(timer);
+ // Clear the atom on unmount to prevent stale callbacks from being retained
+ store.set(tvUserSwitchModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ // Request focus on the first card when ready
+ useEffect(() => {
+ if (isReady && firstCardRef.current) {
+ const timer = setTimeout(() => {
+ (firstCardRef.current as any)?.requestTVFocus?.();
+ }, 50);
+ return () => clearTimeout(timer);
+ }
+ }, [isReady]);
+
+ const handleSelect = (account: SavedServerAccount) => {
+ modalState?.onAccountSelect(account);
+ store.set(tvUserSwitchModalAtom, null);
+ router.back();
+ };
+
+ // If no modal state, just return null
+ if (!modalState) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {t("home.settings.switch_user.title")}
+
+ {modalState.serverName}
+ {isReady && (
+
+ {modalState.accounts.map((account, index) => {
+ const isCurrent = account.userId === modalState.currentUserId;
+ return (
+ handleSelect(account)}
+ />
+ );
+ })}
+
+ )}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ justifyContent: "flex-end",
+ },
+ sheetContainer: {
+ width: "100%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: 24,
+ paddingBottom: 50,
+ overflow: "visible",
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: "500",
+ color: "rgba(255,255,255,0.6)",
+ marginBottom: 4,
+ paddingHorizontal: 48,
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ },
+ subtitle: {
+ fontSize: 14,
+ color: "rgba(255,255,255,0.4)",
+ marginBottom: 16,
+ paddingHorizontal: 48,
+ },
+ scrollView: {
+ overflow: "visible",
+ },
+ scrollContent: {
+ paddingHorizontal: 48,
+ paddingVertical: 20,
+ gap: 16,
+ },
+});
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 79278b702..3d75e67c9 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -2,17 +2,19 @@ import "@/augmentations";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import NetInfo from "@react-native-community/netinfo";
-import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { onlineManager, QueryClient } from "@tanstack/react-query";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import * as BackgroundTask from "expo-background-task";
import * as Device from "expo-device";
+import { DarkTheme, ThemeProvider } from "expo-router/react-navigation";
import { Platform } from "react-native";
import { GlobalModal } from "@/components/GlobalModal";
+import { enableTVMenuKeyInterception } from "@/hooks/useTVBackHandler";
import i18n from "@/i18n";
import { DownloadProvider } from "@/providers/DownloadProvider";
import { GlobalModalProvider } from "@/providers/GlobalModalProvider";
+import { InactivityProvider } from "@/providers/InactivityProvider";
import { IntroSheetProvider } from "@/providers/IntroSheetProvider";
import {
apiAtom,
@@ -54,15 +56,31 @@ import * as TaskManager from "expo-task-manager";
import { Provider as JotaiProvider, useAtom } from "jotai";
import { useCallback, useEffect, useRef, useState } from "react";
import { I18nextProvider } from "react-i18next";
-import { Appearance } from "react-native";
+import { Appearance, LogBox } from "react-native";
import { SystemBars } from "react-native-edge-to-edge";
import { GestureHandlerRootView } from "react-native-gesture-handler";
+
+// Suppress harmless tvOS warning from react-native-gesture-handler
+if (Platform.isTV) {
+ LogBox.ignoreLogs(["HoverGestureHandler is not supported on tvOS"]);
+}
+
import useRouter from "@/hooks/useAppRouter";
import { userAtom } from "@/providers/JellyfinProvider";
-import { store } from "@/utils/store";
+import { store as jotaiStore, store } from "@/utils/store";
import "react-native-reanimated";
+import {
+ configureReanimatedLogger,
+ ReanimatedLogLevel,
+} from "react-native-reanimated";
import { Toaster } from "sonner-native";
+// Disable strict mode warnings for reading shared values during render
+configureReanimatedLogger({
+ level: ReanimatedLogLevel.warn,
+ strict: false,
+});
+
if (!Platform.isTV) {
Notifications.setNotificationHandler({
handleNotification: async () => ({
@@ -178,7 +196,7 @@ export default function RootLayout() {
return (
-
+
@@ -232,6 +250,11 @@ function Layout() {
const _segments = useSegments();
const router = useRouter();
+ // Enable TV menu key interception so React Native handles it instead of tvOS
+ useEffect(() => {
+ enableTVMenuKeyInterception();
+ }, []);
+
useEffect(() => {
i18n.changeLanguage(
settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en",
@@ -252,22 +275,19 @@ function Layout() {
deviceId: getOrSetDeviceId(),
userId: user.Id,
})
- .then((_) => console.log("Posted expo push token"))
.catch((_) =>
writeErrorLog("Failed to push expo push token to plugin"),
);
- } else console.log("No token available");
+ }
}, [api, expoPushToken, user]);
const registerNotifications = useCallback(async () => {
if (Platform.OS === "android") {
- console.log("Setting android notification channel 'default'");
await Notifications?.setNotificationChannelAsync("default", {
name: "default",
});
// Create dedicated channel for download notifications
- console.log("Setting android notification channel 'downloads'");
await Notifications?.setNotificationChannelAsync("downloads", {
name: "Downloads",
importance: Notifications.AndroidImportance.DEFAULT,
@@ -342,8 +362,8 @@ function Layout() {
url = `/(auth)/(tabs)/home/items/page?id=${itemId}`;
// summarized season notification for multiple episodes. Bring them to series season
} else {
- const seriesId = data.seriesId;
- const seasonIndex = data.seasonIndex;
+ const seriesId = data?.seriesId;
+ const seasonIndex = data?.seasonIndex;
if (seasonIndex) {
url = `/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`;
} else {
@@ -375,86 +395,153 @@ function Layout() {
maxAge: 1000 * 60 * 60 * 24, // 24 hours max cache age
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
- // Only persist successful queries
- return query.state.status === "success";
+ return (
+ query.state.status === "success" && query.options.gcTime !== 0
+ );
},
},
}}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
- null,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null,
+ }}
+ />
+ null,
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
- null,
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {!Platform.isTV && }
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/app/login.tsx b/app/login.tsx
index 33d06d41d..d028ae4f2 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -1,659 +1,13 @@
-import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
-import type { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
-import { Image } from "expo-image";
-import { useLocalSearchParams, useNavigation } from "expo-router";
-import { t } from "i18next";
-import { useAtomValue } from "jotai";
-import { useCallback, useEffect, useState } from "react";
-import {
- Alert,
- Keyboard,
- KeyboardAvoidingView,
- Platform,
- Switch,
- TouchableOpacity,
- View,
-} from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
-import { z } from "zod";
-import { Button } from "@/components/Button";
-import { Input } from "@/components/common/Input";
-import { Text } from "@/components/common/Text";
-import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
-import { PreviousServersList } from "@/components/PreviousServersList";
-import { SaveAccountModal } from "@/components/SaveAccountModal";
-import { Colors } from "@/constants/Colors";
-import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
-import type {
- AccountSecurityType,
- SavedServer,
-} from "@/utils/secureCredentials";
+import { Platform } from "react-native";
+import { Login } from "@/components/login/Login";
+import { TVLogin } from "@/components/login/TVLogin";
-const CredentialsSchema = z.object({
- username: z.string().min(1, t("login.username_required")),
-});
-
-const Login: React.FC = () => {
- const api = useAtomValue(apiAtom);
- const navigation = useNavigation();
- const params = useLocalSearchParams();
- const {
- setServer,
- login,
- removeServer,
- initiateQuickConnect,
- loginWithSavedCredential,
- loginWithPassword,
- } = useJellyfin();
-
- const {
- apiUrl: _apiUrl,
- username: _username,
- password: _password,
- } = params as { apiUrl: string; username: string; password: string };
-
- const [loadingServerCheck, setLoadingServerCheck] = useState(false);
- const [loading, setLoading] = useState(false);
- const [serverURL, setServerURL] = useState(_apiUrl || "");
- const [serverName, setServerName] = useState("");
- const [credentials, setCredentials] = useState<{
- username: string;
- password: string;
- }>({
- username: _username || "",
- password: _password || "",
- });
-
- // Save account state
- const [saveAccount, setSaveAccount] = useState(false);
- const [showSaveModal, setShowSaveModal] = useState(false);
- const [pendingLogin, setPendingLogin] = useState<{
- username: string;
- password: string;
- } | null>(null);
-
- /**
- * A way to auto login based on a link
- */
- useEffect(() => {
- (async () => {
- if (_apiUrl) {
- await setServer({
- address: _apiUrl,
- });
-
- // Wait for server setup and state updates to complete
- setTimeout(() => {
- if (_username && _password) {
- setCredentials({ username: _username, password: _password });
- login(_username, _password);
- }
- }, 0);
- }
- })();
- }, [_apiUrl, _username, _password]);
-
- useEffect(() => {
- navigation.setOptions({
- headerTitle: serverName,
- headerLeft: () =>
- api?.basePath ? (
- {
- removeServer();
- }}
- className='flex flex-row items-center pr-2 pl-1'
- >
-
-
- {t("login.change_server")}
-
-
- ) : null,
- });
- }, [serverName, navigation, api?.basePath]);
-
- const handleLogin = async () => {
- Keyboard.dismiss();
-
- const result = CredentialsSchema.safeParse(credentials);
- if (!result.success) return;
-
- if (saveAccount) {
- // Show save account modal to choose security type
- setPendingLogin({
- username: credentials.username,
- password: credentials.password,
- });
- setShowSaveModal(true);
- } else {
- // Login without saving
- await performLogin(credentials.username, credentials.password);
- }
- };
-
- const performLogin = async (
- username: string,
- password: string,
- options?: {
- saveAccount?: boolean;
- securityType?: AccountSecurityType;
- pinCode?: string;
- },
- ) => {
- setLoading(true);
- try {
- await login(username, password, serverName, options);
- } catch (error) {
- if (error instanceof Error) {
- Alert.alert(t("login.connection_failed"), error.message);
- } else {
- Alert.alert(
- t("login.connection_failed"),
- t("login.an_unexpected_error_occured"),
- );
- }
- } finally {
- setLoading(false);
- setPendingLogin(null);
- }
- };
-
- const handleSaveAccountConfirm = async (
- securityType: AccountSecurityType,
- pinCode?: string,
- ) => {
- setShowSaveModal(false);
- if (pendingLogin) {
- await performLogin(pendingLogin.username, pendingLogin.password, {
- saveAccount: true,
- securityType,
- pinCode,
- });
- }
- };
-
- const handleQuickLoginWithSavedCredential = async (
- serverUrl: string,
- userId: string,
- ) => {
- await loginWithSavedCredential(serverUrl, userId);
- };
-
- const handlePasswordLogin = async (
- serverUrl: string,
- username: string,
- password: string,
- ) => {
- await loginWithPassword(serverUrl, username, password);
- };
-
- const handleAddAccount = (server: SavedServer) => {
- // Server is already selected, go to credential entry
- setServer({ address: server.address });
- if (server.name) {
- setServerName(server.name);
- }
- };
-
- /**
- * Checks the availability and validity of a Jellyfin server URL.
- *
- * This function attempts to connect to a Jellyfin server using the provided URL.
- * It tries both HTTPS and HTTP protocols, with a timeout to handle long 404 responses.
- *
- * @param {string} url - The base URL of the Jellyfin server to check.
- * @returns {Promise} A Promise that resolves to:
- * - The full URL (including protocol) if a valid Jellyfin server is found.
- * - undefined if no valid server is found at the given URL.
- *
- * Side effects:
- * - Sets loadingServerCheck state to true at the beginning and false at the end.
- * - Logs errors and timeout information to the console.
- */
- const checkUrl = useCallback(async (url: string) => {
- setLoadingServerCheck(true);
- const baseUrl = url.replace(/^https?:\/\//i, "");
- const protocols = ["https", "http"];
- try {
- return checkHttp(baseUrl, protocols);
- } catch (e) {
- if (e instanceof Error && e.message === "Server too old") {
- throw e;
- }
- return undefined;
- } finally {
- setLoadingServerCheck(false);
- }
- }, []);
-
- async function checkHttp(baseUrl: string, protocols: string[]) {
- for (const protocol of protocols) {
- try {
- const response = await fetch(
- `${protocol}://${baseUrl}/System/Info/Public`,
- {
- mode: "cors",
- },
- );
- if (response.ok) {
- const data = (await response.json()) as PublicSystemInfo;
- const serverVersion = data.Version?.split(".");
- if (serverVersion && +serverVersion[0] <= 10) {
- if (+serverVersion[1] < 10) {
- Alert.alert(
- t("login.too_old_server_text"),
- t("login.too_old_server_description"),
- );
- throw new Error("Server too old");
- }
- }
- setServerName(data.ServerName || "");
- return `${protocol}://${baseUrl}`;
- }
- } catch (e) {
- if (e instanceof Error && e.message === "Server too old") {
- throw e;
- }
- }
- }
- return undefined;
+const LoginPage: React.FC = () => {
+ if (Platform.isTV) {
+ return ;
}
- /**
- * Handles the connection attempt to a Jellyfin server.
- *
- * This function trims the input URL, checks its validity using the `checkUrl` function,
- * and sets the server address if a valid connection is established.
- *
- * @param {string} url - The URL of the Jellyfin server to connect to.
- *
- * @returns {Promise}
- *
- * Side effects:
- * - Calls `checkUrl` to validate the server URL.
- * - Shows an alert if the connection fails.
- * - Sets the server address using `setServer` if the connection is successful.
- *
- */
- const handleConnect = useCallback(async (url: string) => {
- url = url.trim().replace(/\/$/, "");
- try {
- const result = await checkUrl(url);
- if (result === undefined) {
- Alert.alert(
- t("login.connection_failed"),
- t("login.could_not_connect_to_server"),
- );
- return;
- }
- await setServer({ address: result });
- } catch {}
- }, []);
- const handleQuickConnect = async () => {
- try {
- const code = await initiateQuickConnect();
- if (code) {
- Alert.alert(
- t("login.quick_connect"),
- t("login.enter_code_to_login", { code: code }),
- [
- {
- text: t("login.got_it"),
- },
- ],
- );
- }
- } catch (_error) {
- Alert.alert(
- t("login.error_title"),
- t("login.failed_to_initiate_quick_connect"),
- );
- }
- };
-
- return Platform.isTV ? (
- // TV layout
-
-
- {api?.basePath ? (
- // ------------ Username/Password view ------------
-
- {/* Safe centered column with max width so TV doesn’t stretch too far */}
-
-
- {serverName ? (
- <>
- {`${t("login.login_to_title")} `}
- {serverName}
- >
- ) : (
- t("login.login_title")
- )}
-
-
- {api.basePath}
-
-
- {/* Username */}
-
- setCredentials((prev) => ({ ...prev, username: text }))
- }
- onEndEditing={(e) => {
- const newValue = e.nativeEvent.text;
- if (newValue && newValue !== credentials.username) {
- setCredentials((prev) => ({ ...prev, username: newValue }));
- }
- }}
- value={credentials.username}
- keyboardType='default'
- returnKeyType='done'
- autoCapitalize='none'
- autoCorrect={false}
- textContentType='username'
- clearButtonMode='while-editing'
- maxLength={500}
- extraClassName='mb-4'
- autoFocus={false}
- blurOnSubmit={true}
- />
-
- {/* Password */}
-
- setCredentials((prev) => ({ ...prev, password: text }))
- }
- onEndEditing={(e) => {
- const newValue = e.nativeEvent.text;
- if (newValue && newValue !== credentials.password) {
- setCredentials((prev) => ({ ...prev, password: newValue }));
- }
- }}
- value={credentials.password}
- secureTextEntry
- keyboardType='default'
- returnKeyType='done'
- autoCapitalize='none'
- textContentType='password'
- clearButtonMode='while-editing'
- maxLength={500}
- extraClassName='mb-4'
- autoFocus={false}
- blurOnSubmit={true}
- />
-
-
-
-
-
-
-
-
-
- ) : (
- // ------------ Server connect view ------------
-
-
-
-
-
-
-
- Streamyfin
-
-
- {t("server.enter_url_to_jellyfin_server")}
-
-
- {/* Full-width Input with clear focus ring */}
-
-
- {/* Full-width primary button */}
-
-
-
-
- {/* Lists stay full width but inside max width container */}
-
- {
- setServerURL(server.address);
- if (server.serverName) setServerName(server.serverName);
- await handleConnect(server.address);
- }}
- />
- {
- await handleConnect(s.address);
- }}
- onQuickLogin={handleQuickLoginWithSavedCredential}
- onPasswordLogin={handlePasswordLogin}
- onAddAccount={handleAddAccount}
- />
-
-
-
- )}
-
-
- ) : (
- // Mobile layout
-
-
- {api?.basePath ? (
-
-
-
-
- {serverName ? (
- <>
- {`${t("login.login_to_title")} `}
- {serverName}
- >
- ) : (
- t("login.login_title")
- )}
-
- {api.basePath}
-
- setCredentials((prev) => ({ ...prev, username: text }))
- }
- onEndEditing={(e) => {
- const newValue = e.nativeEvent.text;
- if (newValue && newValue !== credentials.username) {
- setCredentials((prev) => ({
- ...prev,
- username: newValue,
- }));
- }
- }}
- value={credentials.username}
- keyboardType='default'
- returnKeyType='done'
- autoCapitalize='none'
- autoCorrect={false}
- textContentType='username'
- clearButtonMode='while-editing'
- maxLength={500}
- />
-
-
- setCredentials((prev) => ({ ...prev, password: text }))
- }
- onEndEditing={(e) => {
- const newValue = e.nativeEvent.text;
- if (newValue && newValue !== credentials.password) {
- setCredentials((prev) => ({
- ...prev,
- password: newValue,
- }));
- }
- }}
- value={credentials.password}
- secureTextEntry
- keyboardType='default'
- returnKeyType='done'
- autoCapitalize='none'
- textContentType='password'
- clearButtonMode='while-editing'
- maxLength={500}
- />
- setSaveAccount(!saveAccount)}
- className='flex flex-row items-center py-2'
- activeOpacity={0.7}
- >
-
-
- {t("save_account.save_for_later")}
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) : (
-
-
-
- Streamyfin
-
- {t("server.enter_url_to_jellyfin_server")}
-
-
-
- {
- setServerURL(server.address);
- if (server.serverName) {
- setServerName(server.serverName);
- }
- await handleConnect(server.address);
- }}
- />
- {
- await handleConnect(s.address);
- }}
- onQuickLogin={handleQuickLoginWithSavedCredential}
- onPasswordLogin={handlePasswordLogin}
- onAddAccount={handleAddAccount}
- />
-
-
- )}
-
-
- {/* Save Account Modal */}
- {
- setShowSaveModal(false);
- setPendingLogin(null);
- }}
- onSave={handleSaveAccountConfirm}
- username={pendingLogin?.username || credentials.username}
- />
-
- );
+ return ;
};
-export default Login;
+export default LoginPage;
diff --git a/app/topshelf/item.tsx b/app/topshelf/item.tsx
new file mode 100644
index 000000000..6f93cf5b7
--- /dev/null
+++ b/app/topshelf/item.tsx
@@ -0,0 +1,33 @@
+import { useLocalSearchParams, useRootNavigationState } from "expo-router";
+import { useEffect } from "react";
+import { View } from "react-native";
+import useRouter from "@/hooks/useAppRouter";
+
+export default function TopShelfItemRedirect() {
+ const router = useRouter();
+ const rootNavigationState = useRootNavigationState();
+ const { id, type } = useLocalSearchParams<{
+ id?: string;
+ type?: string;
+ }>();
+
+ useEffect(() => {
+ if (!rootNavigationState?.key) {
+ return;
+ }
+
+ if (!id) {
+ router.replace("/(auth)/(tabs)/(home)");
+ return;
+ }
+
+ if (type === "Series") {
+ router.replace(`/(auth)/(tabs)/(home)/series/${id}`);
+ return;
+ }
+
+ router.replace(`/(auth)/(tabs)/(home)/items/page?id=${id}`);
+ }, [id, rootNavigationState?.key, router, type]);
+
+ return ;
+}
diff --git a/app/topshelf/play.tsx b/app/topshelf/play.tsx
new file mode 100644
index 000000000..5b848d27a
--- /dev/null
+++ b/app/topshelf/play.tsx
@@ -0,0 +1,32 @@
+import { useLocalSearchParams, useRootNavigationState } from "expo-router";
+import { useEffect } from "react";
+import { View } from "react-native";
+import useRouter from "@/hooks/useAppRouter";
+
+export default function TopShelfPlayRedirect() {
+ const router = useRouter();
+ const rootNavigationState = useRootNavigationState();
+ const { id } = useLocalSearchParams<{
+ id?: string;
+ }>();
+
+ useEffect(() => {
+ if (!rootNavigationState?.key) {
+ return;
+ }
+
+ if (!id) {
+ router.replace("/(auth)/(tabs)/(home)");
+ return;
+ }
+
+ const queryParams = new URLSearchParams({
+ itemId: id,
+ offline: "false",
+ });
+
+ router.replace(`/player/direct-player?${queryParams.toString()}`);
+ }, [id, rootNavigationState?.key, router]);
+
+ return ;
+}
diff --git a/app/tv-account-action-modal.tsx b/app/tv-account-action-modal.tsx
new file mode 100644
index 000000000..87f42dfe7
--- /dev/null
+++ b/app/tv-account-action-modal.tsx
@@ -0,0 +1,251 @@
+import { Ionicons } from "@expo/vector-icons";
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Animated,
+ Easing,
+ Pressable,
+ ScrollView,
+ TVFocusGuideView,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import { tvAccountActionModalAtom } from "@/utils/atoms/tvAccountActionModal";
+import { store } from "@/utils/store";
+
+// Action card component
+const TVAccountActionCard: React.FC<{
+ label: string;
+ icon: keyof typeof Ionicons.glyphMap;
+ variant?: "default" | "destructive";
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+}> = ({ label, icon, variant = "default", hasTVPreferredFocus, onPress }) => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+ const typography = useScaledTVTypography();
+
+ const animateTo = (v: number) =>
+ Animated.timing(scale, {
+ toValue: v,
+ duration: 150,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ const isDestructive = variant === "destructive";
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.05);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ }}
+ hasTVPreferredFocus={hasTVPreferredFocus}
+ >
+
+
+
+ {label}
+
+
+
+ );
+};
+
+export default function TVAccountActionModalPage() {
+ const typography = useScaledTVTypography();
+ const router = useRouter();
+ const modalState = useAtomValue(tvAccountActionModalAtom);
+ const { t } = useTranslation();
+
+ const [isReady, setIsReady] = useState(false);
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(200)).current;
+
+ // Animate in on mount
+ useEffect(() => {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(200);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => {
+ clearTimeout(timer);
+ store.set(tvAccountActionModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ const handleLogin = () => {
+ modalState?.onLogin();
+ router.back();
+ };
+
+ const handleDelete = () => {
+ modalState?.onDelete();
+ router.back();
+ };
+
+ if (!modalState) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {/* Account username as title */}
+
+ {modalState.account.username}
+
+
+ {/* Server name as subtitle */}
+
+ {modalState.server.name || modalState.server.address}
+
+
+ {/* Horizontal options */}
+ {isReady && (
+
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/tv-account-select-modal.tsx b/app/tv-account-select-modal.tsx
new file mode 100644
index 000000000..a8a8e6bd8
--- /dev/null
+++ b/app/tv-account-select-modal.tsx
@@ -0,0 +1,256 @@
+import { Ionicons } from "@expo/vector-icons";
+import { BlurView } from "expo-blur";
+import { useAtomValue } from "jotai";
+import { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Animated,
+ Easing,
+ Pressable,
+ ScrollView,
+ TVFocusGuideView,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { TVUserCard } from "@/components/tv/TVUserCard";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import { tvAccountSelectModalAtom } from "@/utils/atoms/tvAccountSelectModal";
+import { store } from "@/utils/store";
+
+// Action button for bottom sheet
+const TVAccountSelectAction: React.FC<{
+ label: string;
+ icon: keyof typeof Ionicons.glyphMap;
+ variant?: "default" | "destructive";
+ onPress: () => void;
+}> = ({ label, icon, variant = "default", onPress }) => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+ const typography = useScaledTVTypography();
+
+ const animateTo = (v: number) =>
+ Animated.timing(scale, {
+ toValue: v,
+ duration: 150,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ const isDestructive = variant === "destructive";
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.05);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ }}
+ >
+
+
+
+ {label}
+
+
+
+ );
+};
+
+export default function TVAccountSelectModalPage() {
+ const typography = useScaledTVTypography();
+ const router = useRouter();
+ const modalState = useAtomValue(tvAccountSelectModalAtom);
+ const { t } = useTranslation();
+
+ const [isReady, setIsReady] = useState(false);
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(300)).current;
+
+ // Animate in on mount
+ useEffect(() => {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(300);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => {
+ clearTimeout(timer);
+ store.set(tvAccountSelectModalAtom, null);
+ };
+ }, [overlayOpacity, sheetTranslateY]);
+
+ if (!modalState) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {/* Title */}
+
+ {t("server.select_account")}
+
+
+ {/* Server name as subtitle */}
+
+ {modalState.server.name || modalState.server.address}
+
+
+ {/* All options in single horizontal row */}
+ {isReady && (
+
+ {modalState.server.accounts?.map((account, index) => (
+ {
+ modalState.onAccountAction(account);
+ }}
+ hasTVPreferredFocus={index === 0}
+ />
+ ))}
+ {
+ modalState.onAddAccount();
+ router.back();
+ }}
+ />
+ {
+ modalState.onDeleteServer();
+ router.back();
+ }}
+ />
+
+ )}
+
+
+
+
+ );
+}
diff --git a/assets/icons/gear.png b/assets/icons/gear.png
new file mode 100644
index 000000000..f5b98cf07
Binary files /dev/null and b/assets/icons/gear.png differ
diff --git a/assets/images/icon-tvos-small-2x.png b/assets/images/icon-tvos-small-2x.png
new file mode 100644
index 000000000..ba42f64e7
Binary files /dev/null and b/assets/images/icon-tvos-small-2x.png differ
diff --git a/assets/images/icon-tvos-small.png b/assets/images/icon-tvos-small.png
new file mode 100644
index 000000000..8f2300d62
Binary files /dev/null and b/assets/images/icon-tvos-small.png differ
diff --git a/assets/images/icon-tvos-topshelf-2x.png b/assets/images/icon-tvos-topshelf-2x.png
new file mode 100644
index 000000000..611869f9d
Binary files /dev/null and b/assets/images/icon-tvos-topshelf-2x.png differ
diff --git a/assets/images/icon-tvos-topshelf-wide-2x.png b/assets/images/icon-tvos-topshelf-wide-2x.png
new file mode 100644
index 000000000..94c4378e0
Binary files /dev/null and b/assets/images/icon-tvos-topshelf-wide-2x.png differ
diff --git a/assets/images/icon-tvos-topshelf-wide.png b/assets/images/icon-tvos-topshelf-wide.png
new file mode 100644
index 000000000..45bca6fea
Binary files /dev/null and b/assets/images/icon-tvos-topshelf-wide.png differ
diff --git a/assets/images/icon-tvos-topshelf.png b/assets/images/icon-tvos-topshelf.png
new file mode 100644
index 000000000..119fabac4
Binary files /dev/null and b/assets/images/icon-tvos-topshelf.png differ
diff --git a/assets/images/icon-tvos.png b/assets/images/icon-tvos.png
new file mode 100644
index 000000000..df59c8173
Binary files /dev/null and b/assets/images/icon-tvos.png differ
diff --git a/augmentations/number.ts b/augmentations/number.ts
index 11c0837d1..bef44ac58 100644
--- a/augmentations/number.ts
+++ b/augmentations/number.ts
@@ -11,7 +11,7 @@ Number.prototype.bytesToReadable = function (decimals = 2) {
const bytes = this.valueOf();
if (bytes === 0) return "0 Bytes";
- const k = 1024;
+ const k = 1000;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
diff --git a/biome.json b/biome.json
index 6f51bd9b0..6e51af7d5 100644
--- a/biome.json
+++ b/biome.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"files": {
"includes": [
"**/*",
@@ -8,6 +8,8 @@
"!android",
"!Streamyfin.app",
"!utils/jellyseerr",
+ "!expo-env.d.ts",
+ "!modules/**/android/build",
"!.expo",
"!docs/jellyfin-openapi-stable.json"
]
diff --git a/bun-patches/react-native-bottom-tabs@1.2.0.patch b/bun-patches/react-native-bottom-tabs@1.2.0.patch
new file mode 100644
index 000000000..9483b873c
--- /dev/null
+++ b/bun-patches/react-native-bottom-tabs@1.2.0.patch
@@ -0,0 +1,84 @@
+diff --git a/node_modules/react-native-bottom-tabs/.bun-tag-b32ab1c60a5dfcf7 b/.bun-tag-b32ab1c60a5dfcf7
+new file mode 100644
+index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+diff --git a/ios/BottomAccessoryProvider.swift b/ios/BottomAccessoryProvider.swift
+index 539efee7156599e1fc795e11bf411b7dfaf12ec7..b2af39a2e6b014e9b1ae0a51b21115c19280df69 100644
+--- a/ios/BottomAccessoryProvider.swift
++++ b/ios/BottomAccessoryProvider.swift
+@@ -8,7 +8,7 @@ import SwiftUI
+ self.delegate = delegate
+ }
+
+- #if !os(macOS)
++ #if !os(macOS) && !os(tvOS)
+ @available(iOS 26.0, *)
+ public func emitPlacementChanged(_ placement: TabViewBottomAccessoryPlacement?) {
+ var placementValue = "none"
+diff --git a/ios/TabView/NewTabView.swift b/ios/TabView/NewTabView.swift
+index 22c52cdf25ad0f7398d89197cb431ca8dc8e0f99..81411376e68803de8bd83515d42565cfa95daf2b 100644
+--- a/ios/TabView/NewTabView.swift
++++ b/ios/TabView/NewTabView.swift
+@@ -78,11 +78,11 @@ struct ConditionalBottomAccessoryModifier: ViewModifier {
+ }
+
+ func body(content: Content) -> some View {
+- #if os(macOS)
+- // tabViewBottomAccessory is not available on macOS
++ #if os(macOS) || os(tvOS)
++ // tabViewBottomAccessory is not available on macOS or tvOS
+ content
+ #else
+- if #available(iOS 26.0, tvOS 26.0, visionOS 3.0, *), bottomAccessoryView != nil {
++ if #available(iOS 26.0, visionOS 3.0, *), bottomAccessoryView != nil {
+ content
+ .tabViewBottomAccessory {
+ renderBottomAccessoryView()
+@@ -95,7 +95,7 @@ struct ConditionalBottomAccessoryModifier: ViewModifier {
+
+ @ViewBuilder
+ private func renderBottomAccessoryView() -> some View {
+- #if !os(macOS)
++ #if !os(macOS) && !os(tvOS)
+ if let bottomAccessoryView {
+ if #available(iOS 26.0, *) {
+ BottomAccessoryRepresentableView(view: bottomAccessoryView)
+@@ -105,7 +105,7 @@ struct ConditionalBottomAccessoryModifier: ViewModifier {
+ }
+ }
+
+-#if !os(macOS)
++#if !os(macOS) && !os(tvOS)
+ @available(iOS 26.0, *)
+ struct BottomAccessoryRepresentableView: PlatformViewRepresentable {
+ @Environment(\.tabViewBottomAccessoryPlacement) var tabViewBottomAccessoryPlacement
+@@ -135,3 +135,4 @@ struct BottomAccessoryRepresentableView: PlatformViewRepresentable {
+ }
+ }
+ #endif
++
+diff --git a/ios/TabViewImpl.swift b/ios/TabViewImpl.swift
+index 72938be90540ea3a483d7db9a80fb74c04d31272..277278ffdd9268a96cb09869eb1d0c0d5e6ad300 100644
+--- a/ios/TabViewImpl.swift
++++ b/ios/TabViewImpl.swift
+@@ -281,7 +281,7 @@ extension View {
+
+ @ViewBuilder
+ func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View {
+- #if compiler(>=6.2)
++ #if compiler(>=6.2) && !os(tvOS)
+ if #available(iOS 26.0, macOS 26.0, *) {
+ if let behavior {
+ self.tabBarMinimizeBehavior(behavior.convert())
+diff --git a/ios/TabViewProps.swift b/ios/TabViewProps.swift
+index 9cfb29a983b34d3f84fc7a678d19ef4ff30e0325..6a5854483e66200b71722bbac12e100742222bd3 100644
+--- a/ios/TabViewProps.swift
++++ b/ios/TabViewProps.swift
+@@ -6,7 +6,7 @@ internal enum MinimizeBehavior: String {
+ case onScrollUp
+ case onScrollDown
+
+-#if compiler(>=6.2)
++#if compiler(>=6.2) && !os(tvOS)
+ @available(iOS 26.0, macOS 26.0, *)
+ func convert() -> TabBarMinimizeBehavior {
+ #if os(macOS)
diff --git a/bun-patches/react-native-ios-utilities@5.2.0.patch b/bun-patches/react-native-ios-utilities@5.2.0.patch
new file mode 100644
index 000000000..4659493ba
--- /dev/null
+++ b/bun-patches/react-native-ios-utilities@5.2.0.patch
@@ -0,0 +1,28 @@
+diff --git a/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift b/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
+index 09be306d5aa39337c5114c2ad6ba7513218e0751..24ff8ee2c36fef8632a7e012514fd04db9bf89fd 100644
+--- a/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
++++ b/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
+@@ -25,15 +25,14 @@ public extension RCTView {
+ return rootView.recursivelyFindSubview(whereType: targetType);
+ };
+
+- var closestParentReactContentView: RCTRootContentView? {
+- let targetType = RCTRootContentView.self;
+-
+- if let match = self.recursivelyFindParentView(whereType: targetType) {
+- return match;
+- };
+-
+- guard let rootView = self.rootViewForCurrentWindow else { return nil };
+- return rootView.recursivelyFindSubview(whereType: targetType);
++ // PATCH (streamyfin): RCTRootContentView is a legacy paper class that the prebuilt
++ // new-architecture React (RN 0.85) does not export, so any reference to it fails to
++ // link (Undefined symbols: _OBJC_CLASS_$_RCTRootContentView). The app runs the new
++ // architecture, where this content-view lookup is unused; short-circuit to nil.
++ // Return type widened to RCTView? so the caller's `.reactTouchHandlers` (an RCTView
++ // extension) still resolves.
++ var closestParentReactContentView: RCTView? {
++ return nil;
+ };
+
+ var reactTouchHandlers: [RCTTouchHandler]? {
diff --git a/bun-patches/react-native-udp@4.1.7.patch b/bun-patches/react-native-udp@4.1.7.patch
new file mode 100644
index 000000000..823acb86e
--- /dev/null
+++ b/bun-patches/react-native-udp@4.1.7.patch
@@ -0,0 +1,17 @@
+diff --git a/node_modules/react-native-udp/.bun-tag-ea7df8754aa4db91 b/.bun-tag-ea7df8754aa4db91
+new file mode 100644
+index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
+diff --git a/react-native-udp.podspec b/react-native-udp.podspec
+index 7450cc7d0862aadfb47d796929c801a3dc423a57..fa3e42c0152ef2d87536b8c2e484f64d525e35ec 100644
+--- a/react-native-udp.podspec
++++ b/react-native-udp.podspec
+@@ -9,7 +9,8 @@ Pod::Spec.new do |s|
+ s.homepage = package_json["homepage"]
+ s.license = package_json["license"]
+ s.author = { package_json["author"] => package_json["author"] }
+- s.platform = :ios, "7.0"
++ s.ios.deployment_target = "7.0"
++ s.tvos.deployment_target = "15.1"
+ s.source = { :git => package_json["repository"]["url"], :tag => "v#{s.version}" }
+ s.source_files = 'ios/**/*.{h,m}'
+ s.dependency 'React-Core'
diff --git a/bun.lock b/bun.lock
index 326bd749c..ed6a2e46b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,73 +1,74 @@
{
"lockfileVersion": 1,
- "configVersion": 0,
+ "configVersion": 1,
"workspaces": {
"": {
"name": "streamyfin",
"dependencies": {
- "@bottom-tabs/react-navigation": "1.1.0",
+ "@bottom-tabs/react-navigation": "1.2.0",
"@douglowder/expo-av-route-picker-view": "^0.0.5",
- "@expo/metro-runtime": "~6.1.1",
+ "@expo/metro-runtime": "~56.0.13",
"@expo/react-native-action-sheet": "^4.1.1",
- "@expo/ui": "0.2.0-beta.9",
+ "@expo/ui": "~56.0.14",
"@expo/vector-icons": "^15.0.3",
"@gorhom/bottom-sheet": "5.2.8",
"@jellyfin/sdk": "^0.13.0",
- "@react-native-community/netinfo": "^11.4.1",
- "@react-navigation/material-top-tabs": "7.4.9",
- "@react-navigation/native": "^7.0.14",
+ "@react-native-community/netinfo": "^12.0.0",
+ "@react-navigation/native": "^7.2.5",
"@shopify/flash-list": "2.0.2",
- "@tanstack/query-sync-storage-persister": "^5.90.18",
+ "@tanstack/query-sync-storage-persister": "^5.100.14",
"@tanstack/react-pacer": "^0.19.1",
- "@tanstack/react-query": "5.90.20",
- "@tanstack/react-query-persist-client": "^5.90.18",
+ "@tanstack/react-query": "5.100.14",
+ "@tanstack/react-query-persist-client": "^5.100.14",
"axios": "^1.7.9",
- "expo": "~54.0.31",
- "expo-application": "~7.0.8",
- "expo-asset": "~12.0.12",
- "expo-background-task": "~1.0.10",
- "expo-blur": "~15.0.8",
- "expo-brightness": "~14.0.8",
- "expo-build-properties": "~1.0.10",
- "expo-constants": "18.0.13",
- "expo-crypto": "^15.0.8",
- "expo-dev-client": "~6.0.20",
- "expo-device": "~8.0.10",
- "expo-font": "~14.0.10",
- "expo-haptics": "~15.0.8",
- "expo-image": "~3.0.11",
- "expo-linear-gradient": "~15.0.8",
- "expo-linking": "~8.0.11",
- "expo-localization": "~17.0.8",
- "expo-location": "^19.0.8",
- "expo-notifications": "~0.32.16",
- "expo-router": "~6.0.21",
- "expo-screen-orientation": "~9.0.8",
- "expo-secure-store": "^15.0.8",
- "expo-sharing": "~14.0.8",
- "expo-splash-screen": "~31.0.13",
- "expo-status-bar": "~3.0.9",
- "expo-system-ui": "~6.0.9",
- "expo-task-manager": "14.0.9",
- "expo-web-browser": "~15.0.10",
- "i18next": "^25.0.0",
- "jotai": "2.16.2",
- "lodash": "4.17.23",
+ "expo": "~56.0.6",
+ "expo-application": "~56.0.3",
+ "expo-asset": "~56.0.15",
+ "expo-audio": "~56.0.11",
+ "expo-background-task": "~56.0.15",
+ "expo-blur": "~56.0.3",
+ "expo-brightness": "~56.0.5",
+ "expo-build-properties": "~56.0.15",
+ "expo-camera": "~56.0.7",
+ "expo-constants": "~56.0.16",
+ "expo-crypto": "~56.0.4",
+ "expo-dev-client": "~56.0.16",
+ "expo-device": "~56.0.4",
+ "expo-font": "~56.0.5",
+ "expo-haptics": "~56.0.3",
+ "expo-image": "~56.0.9",
+ "expo-linear-gradient": "~56.0.4",
+ "expo-linking": "~56.0.12",
+ "expo-localization": "~56.0.6",
+ "expo-location": "~56.0.14",
+ "expo-notifications": "~56.0.14",
+ "expo-router": "~56.2.7",
+ "expo-screen-orientation": "~56.0.5",
+ "expo-secure-store": "~56.0.4",
+ "expo-sharing": "~56.0.14",
+ "expo-splash-screen": "~56.0.10",
+ "expo-status-bar": "~56.0.4",
+ "expo-system-ui": "~56.0.5",
+ "expo-task-manager": "~56.0.15",
+ "expo-web-browser": "~56.0.5",
+ "i18next": "^26.3.0",
+ "jotai": "2.20.0",
+ "lodash": "4.18.1",
"nativewind": "^2.0.11",
"patch-package": "^8.0.0",
- "react": "19.1.0",
- "react-dom": "19.1.0",
- "react-i18next": "16.5.4",
- "react-native": "0.81.5",
+ "react": "19.2.3",
+ "react-dom": "19.2.3",
+ "react-i18next": "17.0.8",
+ "react-native": "npm:react-native-tvos@0.85.3-0",
"react-native-awesome-slider": "^2.9.0",
- "react-native-bottom-tabs": "1.1.0",
+ "react-native-bottom-tabs": "1.2.0",
"react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2",
"react-native-country-flag": "^2.0.2",
"react-native-device-info": "^15.0.0",
"react-native-draggable-flatlist": "^4.0.3",
"react-native-edge-to-edge": "^1.7.0",
- "react-native-gesture-handler": "2.28.0",
+ "react-native-gesture-handler": "~2.31.1",
"react-native-glass-effect-view": "^1.0.0",
"react-native-google-cast": "^4.9.1",
"react-native-image-colors": "^2.4.0",
@@ -75,12 +76,13 @@
"react-native-ios-utilities": "5.2.0",
"react-native-mmkv": "4.1.1",
"react-native-nitro-modules": "0.33.1",
- "react-native-pager-view": "^6.9.1",
- "react-native-reanimated": "~4.1.1",
+ "react-native-pager-view": "8.0.1",
+ "react-native-qrcode-svg": "^6.3.21",
+ "react-native-reanimated": "4.3.1",
"react-native-reanimated-carousel": "4.0.3",
- "react-native-safe-area-context": "~5.6.0",
- "react-native-screens": "~4.18.0",
- "react-native-svg": "15.12.1",
+ "react-native-safe-area-context": "~5.7.0",
+ "react-native-screens": "4.25.2",
+ "react-native-svg": "15.15.4",
"react-native-text-ticker": "^1.15.0",
"react-native-track-player": "github:lovegaoshi/react-native-track-player#APM",
"react-native-udp": "^4.1.7",
@@ -88,245 +90,203 @@
"react-native-uuid": "^2.0.3",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.21.0",
- "react-native-worklets": "0.5.1",
+ "react-native-worklets": "0.8.3",
"sonner-native": "0.21.2",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
- "zod": "4.1.13",
+ "zod": "4.4.3",
},
"devDependencies": {
- "@babel/core": "7.28.6",
- "@biomejs/biome": "2.3.11",
- "@react-native-community/cli": "20.1.1",
- "@react-native-tvos/config-tv": "0.1.4",
+ "@babel/core": "7.29.7",
+ "@biomejs/biome": "2.4.16",
+ "@react-native-community/cli": "20.1.3",
+ "@react-native-tvos/config-tv": "0.1.6",
"@types/jest": "29.5.14",
- "@types/lodash": "4.17.23",
- "@types/react": "19.1.17",
+ "@types/lodash": "4.17.24",
+ "@types/react": "~19.2.10",
"@types/react-test-renderer": "19.1.0",
"cross-env": "10.1.0",
- "expo-doctor": "1.17.14",
+ "expo-doctor": "1.19.7",
"husky": "9.1.7",
- "lint-staged": "16.2.7",
+ "lint-staged": "17.0.5",
"react-test-renderer": "19.2.3",
"typescript": "5.9.3",
},
},
},
- "overrides": {
- "expo-constants": "18.0.13",
+ "patchedDependencies": {
+ "react-native-ios-utilities@5.2.0": "bun-patches/react-native-ios-utilities@5.2.0.patch",
+ "react-native-udp@4.1.7": "bun-patches/react-native-udp@4.1.7.patch",
+ "react-native-bottom-tabs@1.2.0": "bun-patches/react-native-bottom-tabs@1.2.0.patch",
},
"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=="],
+ "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
- "@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/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
- "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="],
+ "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
- "@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/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@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-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
- "@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/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
- "@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.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
- "@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-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
- "@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.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
- "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="],
+ "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="],
- "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="],
+ "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="],
- "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+ "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
- "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
+ "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
"@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.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-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
- "@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.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
- "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
- "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="],
+ "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og=="],
- "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="],
+ "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
- "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
+ "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
- "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
- "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
- "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
- "@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.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="],
- "@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/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
- "@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.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
- "@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.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-decorators": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg=="],
- "@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-export-default-from": ["@babel/plugin-proposal-export-default-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ=="],
- "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw=="],
-
- "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="],
-
- "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="],
-
- "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="],
-
- "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="],
-
- "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="],
+ "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg=="],
"@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="],
- "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg=="],
+ "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw=="],
- "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA=="],
+ "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ=="],
- "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="],
-
- "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="],
-
- "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="],
-
- "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
-
- "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="],
+ "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
"@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="],
- "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="],
-
- "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="],
-
- "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="],
-
"@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="],
- "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="],
-
- "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="],
-
- "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
+ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
"@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="],
- "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="],
+ "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA=="],
- "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="],
+ "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w=="],
- "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="],
+ "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="],
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="],
- "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="],
+ "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A=="],
"@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="],
- "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="],
+ "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="],
- "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="],
+ "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA=="],
- "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="],
+ "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="],
- "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-flow": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg=="],
+ "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ=="],
- "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw=="],
+ "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q=="],
- "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="],
+ "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
- "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="],
-
- "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="],
-
- "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="],
-
- "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="],
+ "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="],
"@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="],
- "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="],
+ "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A=="],
- "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="],
+ "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="],
- "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="],
+ "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="],
- "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="],
+ "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="],
- "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="],
+ "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug=="],
- "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="],
+ "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA=="],
- "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="],
+ "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q=="],
- "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA=="],
+ "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/types": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A=="],
- "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw=="],
+ "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.29.7", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g=="],
- "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.27.1", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q=="],
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
- "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
- "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+ "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA=="],
- "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="],
+ "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw=="],
- "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="],
-
- "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.28.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w=="],
+ "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q=="],
"@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="],
- "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="],
-
- "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="],
-
"@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg=="],
- "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="],
+ "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="],
"@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="],
- "@babel/preset-react": ["@babel/preset-react@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ=="],
+ "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="],
- "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
+ "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
- "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
+ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
- "@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.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
- "@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/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
- "@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=="],
+ "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
- "@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/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
- "@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/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="],
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="],
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="],
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
-
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
-
- "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@1.1.0", "", { "dependencies": { "color": "^5.0.0" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-+4YppCodABcSNIgJiq95QUQ+3ClVBG+rLG3WmYI0+/nbxqKbCz6luFBep4KFOj98Iplj1JY2Ki6ix8CcOZVQ/Q=="],
+ "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@1.2.0", "", { "dependencies": { "color": "^5.0.0" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-gEnLP7q9Iai0KlVxHDIdlrDgkvJ5vwPzL2+2ucz5BdPWd++Cf5GO1jPq92R4/85PrioviCZnlAD91Wx8WxPOjA=="],
"@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="],
@@ -336,59 +296,77 @@
"@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
- "@expo/cli": ["@expo/cli@54.0.21", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~12.0.13", "@expo/config-plugins": "~54.0.4", "@expo/devcert": "^1.2.1", "@expo/env": "~2.0.8", "@expo/image-utils": "^0.8.8", "@expo/json-file": "^10.0.8", "@expo/metro": "~54.2.0", "@expo/metro-config": "~54.0.13", "@expo/osascript": "^2.3.8", "@expo/package-manager": "^1.9.9", "@expo/plist": "^0.4.8", "@expo/prebuild-config": "^54.0.8", "@expo/schema-utils": "^0.1.8", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.81.5", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "expo-server": "^1.0.5", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.1.6", "minimatch": "^9.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^7.5.2", "terminal-link": "^2.1.1", "undici": "^6.18.2", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-L/FdpyZDsg/Nq6xW6kfiyF9DUzKfLZCKFXEVZcDqCNar6bXxQVotQyvgexRvtUF5nLinuT/UafLOdC3FUALUmA=="],
+ "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.38", "", {}, "sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A=="],
+
+ "@expo/cli": ["@expo/cli@56.1.12", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~56.0.9", "@expo/config-plugins": "~56.0.8", "@expo/devcert": "^1.2.1", "@expo/env": "~2.3.0", "@expo/image-utils": "^0.10.1", "@expo/inline-modules": "^0.0.10", "@expo/json-file": "^10.2.0", "@expo/log-box": "^56.0.12", "@expo/metro": "~56.0.0", "@expo/metro-config": "~56.0.13", "@expo/metro-file-map": "^56.0.3", "@expo/osascript": "^2.6.0", "@expo/package-manager": "^1.12.0", "@expo/plist": "^0.7.0", "@expo/prebuild-config": "^56.0.13", "@expo/require-utils": "^56.1.3", "@expo/router-server": "^56.0.12", "@expo/schema-utils": "^56.0.0", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.4", "@react-native/dev-middleware": "0.85.3", "accepts": "^1.3.8", "arg": "^5.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", "expo-server": "^56.0.4", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^1.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.4", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "main.js" } }, "sha512-Ya/13E1yDx1oAuPw5MDmqzIGyzwSs7KSr1EjgSObOF0VO0GD9jqJjvjOiwurjScLUfxcGZQgq23UzMlBVHwdvA=="],
"@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.6", "", { "dependencies": { "node-forge": "^1.3.3" } }, "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w=="],
- "@expo/config": ["@expo/config@12.0.13", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.4", "@expo/config-types": "^54.0.10", "@expo/json-file": "^10.0.8", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "~3.35.1" } }, "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ=="],
+ "@expo/config": ["@expo/config@56.0.9", "", { "dependencies": { "@expo/config-plugins": "~56.0.8", "@expo/config-types": "^56.0.5", "@expo/json-file": "^10.2.0", "@expo/require-utils": "^56.1.3", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4" } }, "sha512-/lqFeWGSrhpKJVP8tTN8LjuoIe8u8q2w7FzBL0C+wHgl+WM8l1qUIEYWy/sMvsG/NbpUIUsDHJRhQvOkU58eIw=="],
- "@expo/config-plugins": ["@expo/config-plugins@54.0.4", "", { "dependencies": { "@expo/config-types": "^54.0.10", "@expo/json-file": "~10.0.8", "@expo/plist": "^0.4.8", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q=="],
+ "@expo/config-plugins": ["@expo/config-plugins@56.0.8", "", { "dependencies": { "@expo/config-types": "^56.0.5", "@expo/json-file": "~10.2.0", "@expo/plist": "^0.7.0", "@expo/require-utils": "^56.1.3", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "semver": "^7.5.4", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-phTuyBhgVLfqUHMjQkAfRtbyoY6yTxoKja1awtpVnEkoJDxPJuXx1KX5uvq1eZtt4bJQ08OBJ6P95INqRSHpRg=="],
- "@expo/config-types": ["@expo/config-types@54.0.10", "", {}, "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA=="],
+ "@expo/config-types": ["@expo/config-types@56.0.5", "", {}, "sha512-GsAHO/MwW9ZRdgnmyfRXqVGLCP/zejD6rWnp5OROp8mBGRObKm4HfrjlUyT1skjMwCj1OrURx9ZfIc6yeBAkIA=="],
"@expo/devcert": ["@expo/devcert@1.2.1", "", { "dependencies": { "@expo/sudo-prompt": "^9.3.1", "debug": "^3.1.0" } }, "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA=="],
- "@expo/devtools": ["@expo/devtools@0.1.8", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" }, "optionalPeers": ["react", "react-native"] }, "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ=="],
+ "@expo/devtools": ["@expo/devtools@56.0.2", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" }, "optionalPeers": ["react", "react-native"] }, "sha512-ANl4kPdbe0/HQYWkDEN79S6bQhI+i/ZCnPxuC853pPsB4svhINC7Ku9lmGOKPsUUWWnrHg1spkDGQBZ4sD6JxQ=="],
- "@expo/env": ["@expo/env@2.0.8", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0" } }, "sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA=="],
+ "@expo/dom-webview": ["@expo/dom-webview@56.0.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-UIEJxkLg6cHqofKrpWpkn9E6ApxVRtCgZhZkARPr9VV7rBVloJgeroTHs31YgU/JpbI5lLQOnfOlGo54W6C2Ew=="],
- "@expo/fingerprint": ["@expo/fingerprint@0.15.4", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^9.0.0", "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng=="],
+ "@expo/env": ["@expo/env@2.3.0", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-9HnnIbzwTTdbwSjNLXTk0fPm9ZwMJ7c1/31tsni8HZ8Q62KzYCyspahH+V365vg5J6lr001DzNwBxVWSaYCQLg=="],
- "@expo/image-utils": ["@expo/image-utils@0.8.8", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "resolve-global": "^1.0.0", "semver": "^7.6.0", "temp-dir": "~2.0.0", "unique-string": "~2.0.0" } }, "sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA=="],
+ "@expo/expo-modules-macros-plugin": ["@expo/expo-modules-macros-plugin@0.0.9", "", {}, "sha512-odai6D7ng/gA7At8ukFcWcauNEeDdyVqzVPbQxDkyU2NTJ4kgphA4I5iigS5C4LXFicSIzEt2nzdlLM8sjsTdA=="],
- "@expo/json-file": ["@expo/json-file@10.0.8", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ=="],
+ "@expo/fingerprint": ["@expo/fingerprint@0.19.3", "", { "dependencies": { "@expo/env": "^2.3.0", "@expo/spawn-async": "^1.8.0", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-w9Au2IVrtc0Ct+WRa05DVHGNHXYq6VyPZWuFP+5x055OeZ5q6k5Yg+aJ1gfShmjdOhDftRcsvmWmTdTZlWaTZw=="],
- "@expo/metro": ["@expo/metro@54.2.0", "", { "dependencies": { "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-minify-terser": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3" } }, "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w=="],
+ "@expo/image-utils": ["@expo/image-utils@0.10.1", "", { "dependencies": { "@expo/require-utils": "^56.1.3", "@expo/spawn-async": "^1.8.0", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "semver": "^7.6.0" } }, "sha512-YDeefvmYdihS7Wp3ESDUVnOgOSWmj2Cczm9lVNDdm4MqQLdAKm/LPYg83HtFQPfefRlAxyHrQR/O9kIXN9C1Wg=="],
- "@expo/metro-config": ["@expo/metro-config@54.0.13", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~12.0.13", "@expo/env": "~2.0.8", "@expo/json-file": "~10.0.8", "@expo/metro": "~54.2.0", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.29.1", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "minimatch": "^9.0.0", "postcss": "~8.4.32", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-RRufMCgLR2Za1WGsh02OatIJo5qZFt31yCnIOSfoubNc3Qqe92Z41pVsbrFnmw5CIaisv1NgdBy05DHe7pEyuw=="],
+ "@expo/inline-modules": ["@expo/inline-modules@0.0.10", "", { "dependencies": { "@expo/config-plugins": "~56.0.8" } }, "sha512-DKEfq877UTAmL/gOT5aFhlLNDg/EVmTSca7JQMKDGR6mjFSAcrmQf4GJNsi6ztiaqj6cTnIfoSz0hAYdnr6RJQ=="],
- "@expo/metro-runtime": ["@expo/metro-runtime@6.1.2", "", { "dependencies": { "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g=="],
+ "@expo/json-file": ["@expo/json-file@10.2.0", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "json5": "^2.2.3" } }, "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ=="],
- "@expo/osascript": ["@expo/osascript@2.3.8", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "exec-async": "^2.2.0" } }, "sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w=="],
+ "@expo/local-build-cache-provider": ["@expo/local-build-cache-provider@56.0.8", "", { "dependencies": { "@expo/config": "~56.0.9", "chalk": "^4.1.2" } }, "sha512-UsuXwpNi57MNhzZ3be4XThc8xW6nzk3Wu37s1+2qcfZGeJcMLKDFfwO6n8YXeIiGlCsOi0Ee1rsTdgjrKt/YJQ=="],
- "@expo/package-manager": ["@expo/package-manager@1.9.9", "", { "dependencies": { "@expo/json-file": "^10.0.8", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-Nv5THOwXzPprMJwbnXU01iXSrCp3vJqly9M4EJ2GkKko9Ifer2ucpg7x6OUsE09/lw+npaoUnHMXwkw7gcKxlg=="],
+ "@expo/log-box": ["@expo/log-box@56.0.12", "", { "dependencies": { "@expo/dom-webview": "^56.0.5", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-budE6AGmJbpOJfGSOz+JVP3+FevElT82IEIg+ukQ4gZpW/dGO7QX1unFjanKdSaYgudBwJ4FCFGMwWhW/1tXVQ=="],
- "@expo/plist": ["@expo/plist@0.4.8", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.2.3", "xmlbuilder": "^15.1.1" } }, "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ=="],
+ "@expo/metro": ["@expo/metro@56.0.0", "", { "dependencies": { "metro": "0.84.4", "metro-babel-transformer": "0.84.4", "metro-cache": "0.84.4", "metro-cache-key": "0.84.4", "metro-config": "0.84.4", "metro-core": "0.84.4", "metro-file-map": "0.84.4", "metro-minify-terser": "0.84.4", "metro-resolver": "0.84.4", "metro-runtime": "0.84.4", "metro-source-map": "0.84.4", "metro-symbolicate": "0.84.4", "metro-transform-plugins": "0.84.4", "metro-transform-worker": "0.84.4" } }, "sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A=="],
- "@expo/prebuild-config": ["@expo/prebuild-config@54.0.8", "", { "dependencies": { "@expo/config": "~12.0.13", "@expo/config-plugins": "~54.0.4", "@expo/config-types": "^54.0.10", "@expo/image-utils": "^0.8.8", "@expo/json-file": "^10.0.8", "@react-native/normalize-colors": "0.81.5", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg=="],
+ "@expo/metro-config": ["@expo/metro-config@56.0.13", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~56.0.9", "@expo/env": "~2.3.0", "@expo/json-file": "~10.2.0", "@expo/metro": "~56.0.0", "@expo/require-utils": "^56.1.3", "@expo/spawn-async": "^1.8.0", "@jridgewell/gen-mapping": "^0.3.13", "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.5.5", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.33.3", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "msgpackr": "^2.0.1", "picomatch": "^4.0.4", "postcss": "^8.5.14", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-OPyNYiex/6Ms8zT2POdIZsLhcAZYk7O+yJvpz5uG/4QRA7aiESfCy1I+0YHewMlR4P1YQeyxIrfTurs6m9xfZA=="],
+
+ "@expo/metro-file-map": ["@expo/metro-file-map@56.0.3", "", { "dependencies": { "debug": "^4.3.4", "fb-watchman": "^2.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" } }, "sha512-5OGW3z8LgEYgMJOR7F3pC8llFLkb1fVqwAewbCl6S4Vkha8AFQMwOjT+9Wbka+V4rmpljpGqOnMhF4xZbD961w=="],
+
+ "@expo/metro-runtime": ["@expo/metro-runtime@56.0.13", "", { "dependencies": { "@expo/log-box": "^56.0.12", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-aMaFa/RPYm2iQoyYOB5q8AxDmWvf4E2yFbZ6rmBIQWaIPDdixGVUlLQeV8DlDAfZ/j+pNYO7l5M+774WbgkTgg=="],
+
+ "@expo/osascript": ["@expo/osascript@2.6.0", "", { "dependencies": { "@expo/spawn-async": "^1.8.0" } }, "sha512-QvqDBlJXa8CS2vRORJ4wEflY1m0vVI07uSJdIRgBrLxRPBcsrXxrtU7+wXRXMqfq9zLwNP9XbvRsXF2omoDylg=="],
+
+ "@expo/package-manager": ["@expo/package-manager@1.12.0", "", { "dependencies": { "@expo/json-file": "^10.2.0", "@expo/spawn-async": "^1.8.0", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-SWr6093nwBjn94cvElsYZNUnhvs+XtUatUz3h0vAn0IbaWG0B6l/V5ZfOBptX/xq6rMpFG5ibIf/eckLSXw8Gg=="],
+
+ "@expo/plist": ["@expo/plist@0.7.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q=="],
+
+ "@expo/prebuild-config": ["@expo/prebuild-config@56.0.13", "", { "dependencies": { "@expo/config": "~56.0.9", "@expo/config-plugins": "~56.0.8", "@expo/config-types": "^56.0.5", "@expo/image-utils": "^0.10.1", "@expo/json-file": "^10.2.0", "@react-native/normalize-colors": "0.85.3", "debug": "^4.3.1", "expo-modules-autolinking": "~56.0.13", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "sha512-caR1karpDasbNmM+LrcHKZrSnyEYdmxm7kedq+WjiuZg+9XAW5sbEjojo2i9Dq6cfbDJPyr7I0yEprLabnvmpA=="],
"@expo/react-native-action-sheet": ["@expo/react-native-action-sheet@4.1.1", "", { "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "hoist-non-react-statics": "^3.3.0" }, "peerDependencies": { "react": ">=18.0.0" } }, "sha512-4KRaba2vhqDRR7ObBj6nrD5uJw8ePoNHdIOMETTpgGTX7StUbrF4j/sfrP1YUyaPEa1P8FXdwG6pB+2WtrJd1A=="],
- "@expo/schema-utils": ["@expo/schema-utils@0.1.8", "", {}, "sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A=="],
+ "@expo/require-utils": ["@expo/require-utils@56.1.3", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-KyLeOn/zzQSvuPpV5YhB/FPKnpQytno4luN918bGdPDssLBoS3N/0UbC3W0rJAn9kSFu+XpfR81eABRVsSdfgQ=="],
+
+ "@expo/router-server": ["@expo/router-server@56.0.12", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "^56.0.13", "expo": "*", "expo-constants": "^56.0.16", "expo-font": "^56.0.5", "expo-router": "*", "expo-server": "^56.0.4", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["@expo/metro-runtime", "expo-router", "react-dom", "react-server-dom-webpack"] }, "sha512-RqKV2/Z8BH/z8l0ngSpG6//5xxJPaF5dTQvSfPQ0nrvCjikGMeIvyj3B9BeLnmZZhxb3gBtXqrj3irAoiIp2aQ=="],
+
+ "@expo/schema-utils": ["@expo/schema-utils@56.0.1", "", {}, "sha512-CZ/+mYbQmWeOnkCGlWy9K+lFxbJSMFY7+TqBZcKzBSTU5Q7IGRvn/sOG3TdNjIdLPmbA8xe7R/c3UUQ28R9i9w=="],
"@expo/sdk-runtime-versions": ["@expo/sdk-runtime-versions@1.0.0", "", {}, "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="],
- "@expo/spawn-async": ["@expo/spawn-async@1.7.2", "", { "dependencies": { "cross-spawn": "^7.0.3" } }, "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew=="],
+ "@expo/spawn-async": ["@expo/spawn-async@1.8.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw=="],
"@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="],
- "@expo/ui": ["@expo/ui@0.2.0-beta.9", "", { "dependencies": { "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-RaBcp0cMe5GykQogJwRZGy4o4JHDLtrr+HaurDPhwPKqVATsV0rR11ysmFe4QX8XWLP/L3od7NOkXUi5ailvaw=="],
+ "@expo/ui": ["@expo/ui@56.0.14", "", { "dependencies": { "sf-symbols-typescript": "^2.1.0", "vaul": "^1.1.2" }, "peerDependencies": { "@babel/core": "*", "expo": "*", "react": "*", "react-dom": "*", "react-native": "*", "react-native-reanimated": "*", "react-native-worklets": "*" }, "optionalPeers": ["@babel/core", "react-dom", "react-native-reanimated", "react-native-worklets"] }, "sha512-0Wr8nsvk2C+BmhmZDQzYr/hxxddHK+ajuJ7ahacUvxt+gQnEXwbueTm0S/hk/54YGASEgplrPGDuR5zzcY+IZg=="],
- "@expo/vector-icons": ["@expo/vector-icons@15.0.3", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA=="],
+ "@expo/vector-icons": ["@expo/vector-icons@15.1.1", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw=="],
"@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="],
- "@expo/xcpretty": ["@expo/xcpretty@4.3.2", "", { "dependencies": { "@babel/code-frame": "7.10.4", "chalk": "^4.1.0", "find-up": "^5.0.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw=="],
+ "@expo/xcpretty": ["@expo/xcpretty@4.4.4", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "chalk": "^4.1.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw=="],
"@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.8", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-native": "*", "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.16.0 || >=4.0.0-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-+N27SMpbBxXZQ/IA2nlEV6RGxL/qSFHKfdFKcygvW+HqPG5jVNb1OqehLQsGfBP+Up42i0gW5ppI+DhpB7UCzA=="],
@@ -398,36 +376,16 @@
"@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="],
- "@ide/backoff": ["@ide/backoff@1.0.0", "", {}, "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g=="],
-
- "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
-
- "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
-
- "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
-
- "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
+ "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="],
"@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="],
- "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="],
-
- "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="],
-
"@jellyfin/sdk": ["@jellyfin/sdk@0.13.0", "", { "peerDependencies": { "axios": "^1.12.0" } }, "sha512-oiBAOXH6s+dKdReSsYgNktBDzbxtg4JVWhEzIxZSxKcWMdSKmBtK41MhXRO7IWAC40DguKUm3nU/Z493qPAlWA=="],
- "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="],
-
- "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="],
-
"@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="],
- "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="],
-
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
- "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="],
-
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
"@jimp/bmp": ["@jimp/bmp@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "bmp-js": "^0.1.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g=="],
@@ -462,14 +420,26 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
+
+ "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
+
+ "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
+
+ "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
+
+ "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
+
+ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
+
+ "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="],
+
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
- "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
-
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
@@ -498,7 +468,7 @@
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
- "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
+ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
@@ -512,69 +482,69 @@
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
- "@react-native-community/cli": ["@react-native-community/cli@20.1.1", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.1", "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-doctor": "20.1.1", "@react-native-community/cli-server-api": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "@react-native-community/cli-types": "20.1.1", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-aLPUx43+WSeTOaUepR2FBD5a1V0OAZ1QB2DOlRlW4fOEjtBXgv40eM/ho8g3WCvAOKfPvTvx4fZdcuovTyV81Q=="],
+ "@react-native-community/cli": ["@react-native-community/cli@20.1.3", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.3", "@react-native-community/cli-config": "20.1.3", "@react-native-community/cli-doctor": "20.1.3", "@react-native-community/cli-server-api": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "@react-native-community/cli-types": "20.1.3", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg=="],
- "@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-6nGQ08w2+EcDwTFC4JFiW/wI2pLwzMrk9thz4um7tKRNW8sADX0IyCsfM2F4rHS720C0UNKYBZE9nAsfp8Vkcw=="],
+ "@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-sFLdLzapfC0scjgzBJJWYDY2RhHPjuuPkA5r6q0gc/UQH/izXpMpLrhh1DW84cMDraNACK0U62tU7ebNaQ1LMQ=="],
- "@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-ajs2i56MANie/v0bMQ1BmRcrOb6MEvLT2rh/I1CA62NXGqF1Rxv6QwsN84LrADMXHRg8QiEMAIADkyDeQHt7Kg=="],
+ "@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-n73nW0cG92oNF0r994pPqm0DjAShOm3F8LSffDYhJqNAno+h/csmv/37iL4NtSpmKIO8xqsG3uVTXz9X/hzNaQ=="],
- "@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-1iUV2rPAyoWPo8EceAFC2vZTF+pEd9YqS87c0aqpbGOFE0gs1rHEB+auVR8CdjzftR4U9sq6m2jrdst0rvpIkg=="],
+ "@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "fast-glob": "^3.3.2", "fast-xml-parser": "^5.3.6", "picocolors": "^1.1.1" } }, "sha512-DNHDP+OWLyhKShGciBqPcxhxfp1Z/7GQcb4F+TGyCeKQAr+JdnUjRXN3X+YCU/v+g2kbYYyRJKlGabzkVvdrAw=="],
- "@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-doepJgLJVqeJb5tNoP9hyFIcoZ1OMGO7QN/YMuCCIjbThUQe/J87XdwPol3Qrjr58KRt9xeBVz+kHeW5mtSutw=="],
+ "@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-QX9B83nAfCPs0KiaYz61kAEHWr9sttooxzRzNdQwvZTwnsIpvWOT9GvMMj/19OeXiQzMJBzZX0Pgt6+spiUsDQ=="],
- "@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.1", "", { "dependencies": { "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-platform-android": "20.1.1", "@react-native-community/cli-platform-apple": "20.1.1", "@react-native-community/cli-platform-ios": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-eFpg5wWnV7uGqvLemshpgj2trPD8cckqxBuI4nT7sxKF/YpA/e3nnnyytHxPP5EnYfWbMcqfaq8hDJoOnJinGQ=="],
+ "@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.3", "", { "dependencies": { "@react-native-community/cli-config": "20.1.3", "@react-native-community/cli-platform-android": "20.1.3", "@react-native-community/cli-platform-apple": "20.1.3", "@react-native-community/cli-platform-ios": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-EI+mAPWn255/WZ4CQohy1I049yiaxVr41C3BeQ2BCyhxODIDR8XRsLzYb1t9MfqK/C3ZncUN2mPSRXFeKPPI1w=="],
- "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-KPheizJQI0tVvBLy9owzpo+A9qDsDAa87e7a8xNaHnwqGpExnIzFPrbdvrltiZjstU2eB/+/UgNQxYIEd4Oc+g=="],
+ "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.3", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-bzB9ELPOISuqgtDZXFPQlkuxx1YFkNx3cNgslc5ElCrk+5LeCLQLIBh/dmIuK8rwUrPcrramjeBj++Noc+TaAA=="],
- "@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-mQEjOzRFCcQTrCt73Q/+5WWTfUg6U2vLZv5rPuFiNrLbrwRqxVH3OLaXg5gilJkDTJC80z8iOSsdd8MRxONOig=="],
+ "@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.3", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.3", "@react-native-community/cli-tools": "20.1.3", "execa": "^5.0.0", "fast-xml-parser": "^5.3.6", "picocolors": "^1.1.1" } }, "sha512-XJ+DqAD4hkplWVXK5AMgN7pP9+4yRSe5KfZ/b42+ofkDBI55ALlUmX+9HWE3fMuRjcotTCoNZqX2ov97cFDXpQ=="],
- "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.1.1", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.1.1" } }, "sha512-6vr10/oSjKkZO/BBgfFJNQTC/0CDF4WrN8iW9ss+Kt6ZL2QrBXLYz7fobrrboOlHwqqs5EyQadlEaNii7gKRJg=="],
+ "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.1.3", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.1.3" } }, "sha512-2qL48SINotuHbZO73cgqSwqd/OWNx0xTbFSdujhpogV4p8BNwYYypfjh4vJY5qJEB5PxuoVkMXT+aCADpg9nBg=="],
- "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "strict-url-sanitise": "0.0.1", "ws": "^6.2.3" } }, "sha512-phHfiCa4WqfKfaoV2vGVR3ZrYQDQTpI1k+C+i6rXAxFGxPuy8IgFFVOSL543qjKPpHBVwLcA+/xAJCVpdyCtVQ=="],
+ "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.3", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.3", "body-parser": "^2.2.2", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "strict-url-sanitise": "0.0.1", "ws": "^6.2.3" } }, "sha512-hsNsdUKZDd2T99OuNuiXz4VuvLa1UN0zcxefmPjXQgI0byrBLzzDr+o7p03sKuODSzKi2h+BMnUxiS07HACQLA=="],
- "@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.1.1", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-j+zX/H2X+6ZGneIDj56tZ1Hbnip5nSfnq7yGlMyF/zm3U1hKp3G1jN5v0YEfnz/zEmjr7zruh4Y06KmZrF1lrA=="],
+ "@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.1.3", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-EAn0vPCMxtHhfWk2UwLmSUfPfLUnFgC7NjiVJVTKJyVk5qGnkPfoT8te/1IUXFTysUB0F0RIi+NgDB4usFOLeA=="],
- "@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.1", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-Tp+s27I/RDONrGvWVj4IzEmga2HhJhXi8ZlZTfycMMyAcv4LG/CTPira+BUZs8nzLAJNrlJ79pVVPJPqQAe+aw=="],
+ "@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.3", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-IdAcegf0pH1hVraxWTG1ACLkYC0LDQfqtaEf42ESyLIF3Xap70JzL/9tAlxw7lSCPZPFWhrcgU0TBc4SkC/ecw=="],
- "@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="],
+ "@react-native-community/netinfo": ["@react-native-community/netinfo@12.0.1", "", { "peerDependencies": { "react": "*", "react-native": ">=0.59" } }, "sha512-P/3caXIvfYSJG8AWJVefukg+ZGRPs+M4Lp3pNJtgcTYoJxCjWrKQGNnCkj/Cz//zWa/avGed0i/wzm0T8vV2IQ=="],
- "@react-native-tvos/config-tv": ["@react-native-tvos/config-tv@0.1.4", "", { "dependencies": { "getenv": "^1.0.0" }, "peerDependencies": { "expo": ">=52.0.0" } }, "sha512-xfVDqSFjEUsb+xcMk0hE2Z/M6QZH0QzAJOSQZwo7W/ZRaLrd+xFQnx0LaXqt3kxlR3P7wskKHByDP/FSoUZnbA=="],
+ "@react-native-masked-view/masked-view": ["@react-native-masked-view/masked-view@0.3.2", "", { "peerDependencies": { "react": ">=16", "react-native": ">=0.57" } }, "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ=="],
- "@react-native/assets-registry": ["@react-native/assets-registry@0.81.5", "", {}, "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w=="],
+ "@react-native-tvos/config-tv": ["@react-native-tvos/config-tv@0.1.6", "", { "dependencies": { "getenv": "^1.0.0", "glob": "^11.0.0" }, "peerDependencies": { "expo": ">=52.0.0" } }, "sha512-VxMSIcro+U1EVb64pYShZsc+uE3HNGhfHppoUhTyGwx9ELQkhWvReRTOI4gpb/qeRWEcT+UbUc9Gd9Zlwm572w=="],
- "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.5" } }, "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ=="],
+ "@react-native-tvos/virtualized-lists": ["@react-native-tvos/virtualized-lists@0.85.3-0", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "0.85.3" }, "optionalPeers": ["@types/react"] }, "sha512-4Ifp8SCnvJnH+4SGwhpwFa1dzt3dh0uQ3+tdLKVDKL3yuOmbNCjUQ09q7i0+5r57tPoFKb4xmaW+7yKHaSTsfA=="],
- "@react-native/babel-preset": ["@react-native/babel-preset@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.81.5", "babel-plugin-syntax-hermes-parser": "0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA=="],
+ "@react-native/assets-registry": ["@react-native/assets-registry@0.85.3", "", {}, "sha512-u9ZiYP23vA2IFtdFQFmetzSmk6SM0xgKIoiOsr1hXNHjHaLhOm+/Ph1ud57wX6+Dbwdzx8coJgnzSKL3W21PCg=="],
- "@react-native/codegen": ["@react-native/codegen@0.81.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g=="],
+ "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.85.3", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@react-native/codegen": "0.85.3" } }, "sha512-Wc94zGfeFG8Njf9SHMPfYZP04kjigkOps6F1TYTvd7ZVXuGxqseCDgxc50LWcOhOCLypI9n3oVVqz81C3p44ZA=="],
- "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.81.5", "", { "dependencies": { "@react-native/dev-middleware": "0.81.5", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.1", "metro-config": "^0.83.1", "metro-core": "^0.83.1", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw=="],
+ "@react-native/babel-preset": ["@react-native/babel-preset@0.85.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@react-native/babel-plugin-codegen": "0.85.3", "babel-plugin-syntax-hermes-parser": "0.33.3", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw=="],
- "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.81.5", "", {}, "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w=="],
+ "@react-native/codegen": ["@react-native/codegen@0.85.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.29.0", "hermes-parser": "0.33.3", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "tinyglobby": "^0.2.15", "yargs": "^17.6.2" } }, "sha512-/JkS1lGLyzBWP1FbgDwaqEf7qShIC6pUC1M0a/YMAd/v4iqR24MRkQWe7jkYvcBQ2LpEhs5NGE9InhxSv21zCA=="],
- "@react-native/dev-middleware": ["@react-native/dev-middleware@0.81.5", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.81.5", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA=="],
+ "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.85.3", "", { "dependencies": { "@react-native/dev-middleware": "0.85.3", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.84.3", "metro-config": "^0.84.3", "metro-core": "^0.84.3", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "0.85.3" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-fs85dmbIqNmtzEixDb0g+q6R3Vt4H9eAt8/inIZdDKfjN76+sUJA2r1nxODQ76bU23MrIbz8sI7KFBPaWk/zQw=="],
- "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.81.5", "", {}, "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg=="],
+ "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.85.3", "", {}, "sha512-uAu7rM5o/Np1zgp6fi5zM1sP1aB8DcS7DdOLcj/TkSutOAjkMqqd2lWt1/+3S7qXexRHVK5XcP+o3VXo4L/V0A=="],
- "@react-native/js-polyfills": ["@react-native/js-polyfills@0.81.5", "", {}, "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w=="],
+ "@react-native/debugger-shell": ["@react-native/debugger-shell@0.85.3", "", { "dependencies": { "cross-spawn": "^7.0.6", "debug": "^4.4.0", "fb-dotslash": "0.5.8" } }, "sha512-/jRAaT9boiCttIcEwS02WPwYkUihqsjSaK/TMtHz05vT6uMgac9PaQt5kzBQLIABv5aEIa5gtrMmKVz49MjkjQ=="],
- "@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.5", "", {}, "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g=="],
+ "@react-native/dev-middleware": ["@react-native/dev-middleware@0.85.3", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.85.3", "@react-native/debugger-shell": "0.85.3", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.3.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-JYzBiT4A8w+KQt+dOD5v+ti+tDrGoPnsSTuApq3Ls4RB5sfWbDlYMyz3dbc8qBIHz9tv0sQ5+eOu6Xwqzr5AQA=="],
- "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.81.5", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw=="],
+ "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.85.3", "", {}, "sha512-39dY2j50Q1pntejzwt3XL7vwXtrj8jcIfHq6E+gyu3jzYxZJVvMkMutQ39vSg6zinIQOX36oQDhidXUbCXzgoA=="],
- "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.8.4", "", { "dependencies": { "@react-navigation/elements": "^2.8.1", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-Ie+7EgUxfZmVXm4RCiJ96oaiwJVFgVE8NJoeUKLLcYEB/99wKbhuKPJNtbkpR99PHfhq64SE7476BpcP4xOFhw=="],
+ "@react-native/js-polyfills": ["@react-native/js-polyfills@0.85.3", "", {}, "sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A=="],
- "@react-navigation/core": ["@react-navigation/core@7.13.0", "", { "dependencies": { "@react-navigation/routers": "^7.5.1", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-Fc/SO23HnlGnkou/z8JQUzwEMvhxuUhr4rdPTIZp/c8q1atq3k632Nfh8fEiGtk+MP1wtIvXdN2a5hBIWpLq3g=="],
+ "@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.85.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.85.3", "hermes-parser": "0.33.3", "nullthrows": "^1.1.1" } }, "sha512-omuKq+r7jM4XvCMIlNMPP7Up3SyB8o5EAdZtF7YXniKyq7UOMBqhYHFqgsdOXr0lT+3ADf7VCJG3sb82jlBrrQ=="],
- "@react-navigation/elements": ["@react-navigation/elements@2.9.2", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.25", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-J1GltOAGowNLznEphV/kr4zs0U7mUBO1wVA2CqpkN8ePBsoxrAmsd+T5sEYUCXN9KgTDFvc6IfcDqrGSQngd/g=="],
+ "@react-native/metro-config": ["@react-native/metro-config@0.85.3", "", { "dependencies": { "@react-native/js-polyfills": "0.85.3", "@react-native/metro-babel-transformer": "0.85.3", "metro-config": "^0.84.3", "metro-runtime": "^0.84.3" } }, "sha512-sVo6HepUmCcpdfozEf91lA0FjpLNNZYu/Zi9FiYiAQTK8pzATXDVTqhvdxpFrQn435p5eUTSbllvbH/KN+bnyA=="],
- "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.4.9", "", { "dependencies": { "@react-navigation/elements": "^2.9.2", "color": "^4.2.3", "react-native-tab-view": "^4.2.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.25", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-oYpdTfa2D1Tn0HJER9dRCR260agKGgYe+ydSHt3RIsJ9sLg8hU7ntKYWo1FnEC/Nsv1/N1u/tRst7ZpQRjjl4A=="],
+ "@react-native/normalize-colors": ["@react-native/normalize-colors@0.85.3", "", {}, "sha512-hj0PScZEhIbcOvQV5yMKX3ha4XEIOy/SVE1Rrpp0beW0dpNLOgSC7KDxGewmDnIHK9YdQUXGY9eMEfShUMIaZw=="],
- "@react-navigation/native": ["@react-navigation/native@7.1.19", "", { "dependencies": { "@react-navigation/core": "^7.13.0", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-fM7q8di4Q8sp2WUhiUWOe7bEDRyRhbzsKQOd5N2k+lHeCx3UncsRYuw4Q/KN0EovM3wWKqMMmhy/YWuEO04kgw=="],
+ "@react-navigation/core": ["@react-navigation/core@7.17.5", "", { "dependencies": { "@react-navigation/routers": "^7.5.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-6fDCwDTWC7DJn0SDb9DJGRlipaygHIc+2elpZBJI6Crl/2Pu+Z1d6W4jMJ2gZO6iHKf+Pe5sUiQ/uwepGprZtg=="],
- "@react-navigation/native-stack": ["@react-navigation/native-stack@7.6.2", "", { "dependencies": { "@react-navigation/elements": "^2.8.1", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-CB6chGNLwJYiyOeyCNUKx33yT7XJSwRZIeKHf4S1vs+Oqu3u9zMnvGUIsesNgbgX0xy16gBqYsrWgr0ZczBTtA=="],
+ "@react-navigation/native": ["@react-navigation/native@7.2.5", "", { "dependencies": { "@react-navigation/core": "^7.17.5", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg=="],
- "@react-navigation/routers": ["@react-navigation/routers@7.5.1", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w=="],
+ "@react-navigation/routers": ["@react-navigation/routers@7.5.5", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ=="],
"@shopify/flash-list": ["@shopify/flash-list@2.0.2", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w=="],
@@ -584,43 +554,39 @@
"@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="],
- "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
+ "@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="],
- "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="],
+ "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="],
- "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
+ "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="],
- "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.100.14", "", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="],
- "@tanstack/pacer": ["@tanstack/pacer@0.17.1", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-52GytGu07L73lNCWB1N02NWBp/tzK2jZ20U8sFInXyiq2KHtHxbXaN1Qw/MR1REqFIKgEy5DOBNZRjuSy5zaRg=="],
+ "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" } }, "sha512-mn60cqoQO/xB6aHxp/hxlSj5mcdcTO4tjj4SXSz5MKzkaMZnvcEGySz3+cGQOT8McREN56fL41L0eR//v5RwNw=="],
- "@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="],
+ "@tanstack/query-sync-storage-persister": ["@tanstack/query-sync-storage-persister@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14", "@tanstack/query-persist-client-core": "5.100.14" } }, "sha512-sDsiVjLJqslUdqIANGvRyB4hYpAooYj5R1fe2EzKfrSY7XufSe+AFBvirLgX/nL2uS1JeP4XeyUuG3TM0bAN9w=="],
- "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.91.15", "", { "dependencies": { "@tanstack/query-core": "5.90.16" } }, "sha512-vnPSfQVo41EKJN8v20nkhWNZPyB1dMJIy5icOvCGzcCJzsmRefYY1owtr63ICOcjOiPPTuNEfPsdjdBhkzYnmA=="],
+ "@tanstack/react-pacer": ["@tanstack/react-pacer@0.19.4", "", { "dependencies": { "@tanstack/pacer": "0.18.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-coj8ULAuR0qFpjAKD44gTgRuZyjxU6Xu+IX5MwwYvr4e61OtZcJshaExoOBKpCGde0Edb12jDnzzj2Im13Qm9Q=="],
- "@tanstack/query-sync-storage-persister": ["@tanstack/query-sync-storage-persister@5.90.18", "", { "dependencies": { "@tanstack/query-core": "5.90.16", "@tanstack/query-persist-client-core": "5.91.15" } }, "sha512-tKngFopz/TuAe7LBDg7IOhWPh9blxdQ6QG/vVL2dFzRmlPNcSo4WdCSONqSDioJkcyTwh1YCSlcikmJ1WnSb3Q=="],
+ "@tanstack/react-query": ["@tanstack/react-query@5.100.14", "", { "dependencies": { "@tanstack/query-core": "5.100.14" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw=="],
- "@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-persist-client": ["@tanstack/react-query-persist-client@5.100.14", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.100.14" }, "peerDependencies": { "@tanstack/react-query": "^5.100.14", "react": "^18 || ^19" } }, "sha512-lQSnbJva85o7jGcJiIDrA8s3VGGx9zaBCgAljm0H1QcScU2iaDYnPuRLg/xI0k0dC45pgg9RTvpgJx5iVHRsjA=="],
- "@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="],
+ "@tanstack/react-store": ["@tanstack/react-store@0.8.1", "", { "dependencies": { "@tanstack/store": "0.8.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig=="],
- "@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/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="],
- "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
- "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
- "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
- "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
-
- "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
-
- "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
-
- "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="],
+ "@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="],
"@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="],
@@ -634,51 +600,47 @@
"@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.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="],
+ "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
- "@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
+ "@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
- "@types/react": ["@types/react@19.1.17", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA=="],
+ "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
"@types/react-test-renderer": ["@types/react-test-renderer@19.1.0", "", { "dependencies": { "@types/react": "*" } }, "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ=="],
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
- "@types/yargs": ["@types/yargs@17.0.34", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A=="],
+ "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
- "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
- "@urql/core": ["@urql/core@5.2.0", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.13", "wonka": "^6.3.2" } }, "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A=="],
+ "@vibrant/color": ["@vibrant/color@4.0.4", "", {}, "sha512-Fq2tAszz4QOPWfHZ+KuEAchXUD8i594BM2fOJt8dI/fvYbiVoBycBF/BlNH6F4IWBubxXoPqD4JmmAHvFYbNew=="],
- "@urql/exchange-retry": ["@urql/exchange-retry@1.3.2", "", { "dependencies": { "@urql/core": "^5.1.2", "wonka": "^6.3.2" } }, "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg=="],
+ "@vibrant/core": ["@vibrant/core@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/generator": "^4.0.4", "@vibrant/image": "^4.0.4", "@vibrant/quantizer": "^4.0.4", "@vibrant/worker": "^4.0.4" } }, "sha512-yZ0XSpW2biKyaJPpBC31AVYgn7NseKSO2q3KNMmDrkL2qC6TEWsBMnSQ28n0m///chZELXpQLx1CCOsWg5pj8w=="],
- "@vibrant/color": ["@vibrant/color@4.0.0", "", {}, "sha512-S9ItdqS1135wTXoIIqAJu8df9dqlOo6Boc5Y4MGsBTu9UmUOvOwfj5b4Ga6S5yrLAKmKYIactkz7zYJdMddkig=="],
+ "@vibrant/generator": ["@vibrant/generator@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/types": "^4.0.4" } }, "sha512-rwq8PnlpKdch4YqaA1FAwdm71gKE2cMrUsbu72TqRFGa8rpP1roaZlQCVXIIwElXVc3r9axZyAcqyTLaMjhrTg=="],
- "@vibrant/core": ["@vibrant/core@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/generator": "^4.0.0", "@vibrant/image": "^4.0.0", "@vibrant/quantizer": "^4.0.0", "@vibrant/worker": "^4.0.0" } }, "sha512-fqlVRUTDjEws9VNKvI3cDXM4wUT7fMFS+cVqEjJk3im+R5EvjJzPF6OAbNhfPzW04NvHNE555eY9FfhYuX3PRw=="],
+ "@vibrant/generator-default": ["@vibrant/generator-default@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/generator": "^4.0.4" } }, "sha512-QeVDeH2dz9lityvJCb84Ml4hlBTElwCpU7SVpiDFBh6gPoCLnzcb1H9G4NgG3hOlAPyrBM+Ivq1Pg+1lZj5Ywg=="],
- "@vibrant/generator": ["@vibrant/generator@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/types": "^4.0.0" } }, "sha512-CqKAjmgHVDXJVo3Q5+9pUJOvksR7cN3bzx/6MbURYh7lA4rhsIewkUK155M6q0vfcUN3ETi/eTneCi0tLuM2Sg=="],
+ "@vibrant/image": ["@vibrant/image@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4" } }, "sha512-NBIJj7umfDRVpFjJHQo1AFSCWCzQyjfil+Yxu7W62PEL72GPCif0CDiglPkvVF8QhDLmnx/x1k3LIBb9jWF2sw=="],
- "@vibrant/generator-default": ["@vibrant/generator-default@4.0.3", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/generator": "^4.0.0" } }, "sha512-HZlfp19sDokODEkZF4p70QceARHgjP3a1Dmxg+dlblYMJM98jPq+azA0fzqKNR7R17JJNHxexpJEepEsNlG0gw=="],
+ "@vibrant/image-browser": ["@vibrant/image-browser@4.0.4", "", { "dependencies": { "@vibrant/image": "^4.0.4" } }, "sha512-7qVyAm+z9t98iwMDzUgGCwgRg0KBB5RXQFgiO2Um5Izd1wO7BKC0SHVEz2k7sRx3XNfBf+JExp8quPrvSz17gg=="],
- "@vibrant/image": ["@vibrant/image@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0" } }, "sha512-Asv/7R/L701norosgvbjOVkodFiwcFihkXixA/gbAd6C+5GCts1Wm1NPk14FNKnM7eKkfAN+0wwPkdOH+PY/YA=="],
+ "@vibrant/image-node": ["@vibrant/image-node@4.0.4", "", { "dependencies": { "@jimp/custom": "^0.22.12", "@jimp/plugin-resize": "^0.22.12", "@jimp/types": "^0.22.12", "@vibrant/image": "^4.0.4" } }, "sha512-aG8Ukt9oTa6FWaAV5oBKsBetkKASWH31hZiFJ2R1291f3TZlphUyQTJz5TubucIRsCEl4dgG1xyxFPgse2IABA=="],
- "@vibrant/image-browser": ["@vibrant/image-browser@4.0.0", "", { "dependencies": { "@vibrant/image": "^4.0.0" } }, "sha512-mXckzvJWiP575Y/wNtP87W/TPgyJoGlPBjW4E9YmNS6n4Jb6RqyHQA0ZVulqDslOxjSsihDzY7gpAORRclaoLg=="],
+ "@vibrant/quantizer": ["@vibrant/quantizer@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/image": "^4.0.4", "@vibrant/types": "^4.0.4" } }, "sha512-722CooC2W4mlBiv+zyAsIrIvARnMCN/P2Muo8bnWd0SQlVWFtQnFxJWGOUPOPS4DGe3pGoqmNfvS0let4dICZQ=="],
- "@vibrant/image-node": ["@vibrant/image-node@4.0.0", "", { "dependencies": { "@jimp/custom": "^0.22.12", "@jimp/plugin-resize": "^0.22.12", "@jimp/types": "^0.22.12", "@vibrant/image": "^4.0.0" } }, "sha512-m7yfnQtmo2y8z+tOjRFBx6q/qGnhl/ax2uCaj4TBkm4TtXfR4Dsn90wT6OWXmCFFzxIKHXKKEBShkxR+4RHseA=="],
+ "@vibrant/quantizer-mmcq": ["@vibrant/quantizer-mmcq@4.0.4", "", { "dependencies": { "@vibrant/color": "^4.0.4", "@vibrant/image": "^4.0.4", "@vibrant/quantizer": "^4.0.4" } }, "sha512-/1CNnM96J8K+OBCWNUzywo6VdnmdFJyiKO+ty/nkfe8H0NseOEHIL7PrVtWGgtsb0rh2uTAq2rjXv65TfgPy8g=="],
- "@vibrant/quantizer": ["@vibrant/quantizer@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/image": "^4.0.0", "@vibrant/types": "^4.0.0" } }, "sha512-YDGxmCv/RvHFtZghDlVRwH5GMxdGGozWS1JpUOUt73/F5zAKGiiier8F31K1npIXARn6/Gspvg/Rbg7qqyEr2A=="],
+ "@vibrant/types": ["@vibrant/types@4.0.4", "", {}, "sha512-Qq3mVTJamn7yD4OBgBEUKaxfDlm3sxBK55N7dH3XzI9Ey7KR00R06uwtqOcEJMsziWTEXdYN3VUlYaj2Tkt7hw=="],
- "@vibrant/quantizer-mmcq": ["@vibrant/quantizer-mmcq@4.0.0", "", { "dependencies": { "@vibrant/color": "^4.0.0", "@vibrant/image": "^4.0.0", "@vibrant/quantizer": "^4.0.0" } }, "sha512-TZqNiRoGGyCP8fH1XE6rvhFwLNv9D8MP1Xhz3K8tsuUweC6buWax3qLfrfEnkhtQnPJHaqvTfTOlIIXVMfRpow=="],
+ "@vibrant/worker": ["@vibrant/worker@4.0.4", "", { "dependencies": { "@vibrant/types": "^4.0.4" } }, "sha512-Q/R6PYhSMWCXEk/IcXbWIzIu7Z4b58ABkGvcdF8Y+q/7g+KnpxKW5x/jfQ/6ciyYSby13wZZoEdNr3QQVgsdBQ=="],
- "@vibrant/types": ["@vibrant/types@4.0.0", "", {}, "sha512-tA5TAbuROXcPkt+PWjmGfoaiEXyySVaNnCZovf6vXhCbMdrTTCQXvNCde2geiVl6YwtuU/Qrj9iZxS5jZ6yVIw=="],
+ "@vscode/sudo-prompt": ["@vscode/sudo-prompt@9.3.2", "", {}, "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw=="],
- "@vibrant/worker": ["@vibrant/worker@4.0.0", "", { "dependencies": { "@vibrant/types": "^4.0.0" } }, "sha512-nSaZZwWQKOgN/nPYUAIRF0/uoa7KpK91A+gjLmZZDgfN1enqxaiihmn+75ayNadW0c6cxAEpEFEHTONR5u9tMw=="],
-
- "@vscode/sudo-prompt": ["@vscode/sudo-prompt@9.3.1", "", {}, "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA=="],
-
- "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
+ "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
@@ -686,15 +648,13 @@
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
- "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
- "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
-
- "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
+ "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="],
- "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
+ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-fragments": ["ansi-fragments@0.2.1", "", { "dependencies": { "colorette": "^1.0.7", "slice-ansi": "^2.0.0", "strip-ansi": "^5.0.0" } }, "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w=="],
@@ -716,9 +676,9 @@
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
- "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
- "assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
+ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
"astral-regex": ["astral-regex@1.0.0", "", {}, "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="],
@@ -726,45 +686,33 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
- "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
+ "axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="],
- "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
-
- "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="],
-
- "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="],
-
- "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="],
-
- "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="],
+ "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.17", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w=="],
"babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="],
- "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="],
+ "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.8", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="],
- "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.29.1", "", { "dependencies": { "hermes-parser": "0.29.1" } }, "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA=="],
+ "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.33.3", "", { "dependencies": { "hermes-parser": "0.33.3" } }, "sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA=="],
"babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="],
- "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="],
-
- "babel-preset-expo": ["babel-preset-expo@54.0.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.81.5", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.29.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo"] }, "sha512-8J6hRdgEC2eJobjoft6mKJ294cLxmi3khCUy2JJQp4htOYYkllSLUq6vudWJkTJiIuGdVR4bR6xuz2EvJLWHNg=="],
-
- "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
+ "babel-preset-expo": ["babel-preset-expo@56.0.13", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.28.6", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-plugin-codegen": "0.85.3", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.33.3", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^56.0.15", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-+CxxAQrN95N+/dF4AUJXNxEh5cEv4yhxb4CM5ijdc2OeIIw+hxzYh2OM1X7QHIm6hkT66H4vJCTT636yjJ8MnQ=="],
"badgin": ["badgin@1.2.3", "", {}, "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ "barcode-detector": ["barcode-detector@3.1.3", "", { "dependencies": { "zxing-wasm": "3.0.3" } }, "sha512-omL3/x26oU9jlR0gUQcGdXIjQtMlrUGKF7xRFO1RwrQkRkRU7WLz0mgQEsdUtYBm2uX3JH+HQLrKlyTS/BxZRw=="],
+
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
- "baseline-browser-mapping": ["baseline-browser-mapping@2.8.25", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA=="],
-
- "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="],
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
@@ -774,7 +722,7 @@
"bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="],
- "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
@@ -782,11 +730,11 @@
"bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="],
- "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+ "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
- "browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="],
+ "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="],
@@ -796,7 +744,7 @@
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
- "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
+ "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@@ -810,17 +758,15 @@
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
- "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
-
"chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="],
- "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="],
+ "chromium-edge-launcher": ["chromium-edge-launcher@0.3.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4" } }, "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
@@ -828,7 +774,7 @@
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
- "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
+ "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
@@ -836,15 +782,15 @@
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
- "color": ["color@5.0.2", "", { "dependencies": { "color-convert": "^3.0.1", "color-string": "^2.0.0" } }, "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA=="],
+ "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="],
- "color-convert": ["color-convert@3.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg=="],
+ "color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="],
- "color-name": ["color-name@2.0.2", "", {}, "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A=="],
+ "color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="],
- "color-string": ["color-string@2.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA=="],
+ "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="],
- "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
+ "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
@@ -856,17 +802,15 @@
"compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="],
- "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
-
"connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
- "core-js-compat": ["core-js-compat@3.46.0", "", { "dependencies": { "browserslist": "^4.26.3" } }, "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law=="],
+ "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="],
- "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
+ "cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="],
"cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="],
@@ -874,8 +818,6 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
- "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
-
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
"css-in-js-utils": ["css-in-js-utils@3.1.0", "", { "dependencies": { "hyphenate-style-name": "^1.0.3" } }, "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A=="],
@@ -890,11 +832,13 @@
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
- "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
- "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
+ "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -902,22 +846,18 @@
"decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="],
- "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
-
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
- "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
-
- "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
-
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -928,8 +868,14 @@
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
+ "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
+
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
+ "dnssd-advertise": ["dnssd-advertise@1.1.4", "", {}, "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA=="],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -938,17 +884,11 @@
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
- "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
-
- "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
-
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
- "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
-
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.249", "", {}, "sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.363", "", {}, "sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -956,11 +896,9 @@
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
- "env-editor": ["env-editor@0.4.2", "", {}, "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA=="],
-
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
- "envinfo": ["envinfo@7.20.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg=="],
+ "envinfo": ["envinfo@7.21.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
@@ -968,13 +906,13 @@
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
- "errorhandler": ["errorhandler@1.5.1", "", { "dependencies": { "accepts": "~1.3.7", "escape-html": "~1.0.3" } }, "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A=="],
+ "errorhandler": ["errorhandler@1.5.2", "", { "dependencies": { "accepts": "~1.3.8", "escape-html": "~1.0.3" } }, "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
- "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
@@ -984,103 +922,109 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
- "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
-
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
- "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
+ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
- "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="],
-
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
"expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="],
- "expo": ["expo@54.0.31", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.21", "@expo/config": "~12.0.13", "@expo/config-plugins": "~54.0.4", "@expo/devtools": "0.1.8", "@expo/fingerprint": "0.15.4", "@expo/metro": "~54.2.0", "@expo/metro-config": "54.0.13", "@expo/vector-icons": "^15.0.3", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~54.0.9", "expo-asset": "~12.0.12", "expo-constants": "~18.0.13", "expo-file-system": "~19.0.21", "expo-font": "~14.0.10", "expo-keep-awake": "~15.0.8", "expo-modules-autolinking": "3.0.24", "expo-modules-core": "3.0.29", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-kQ3RDqA/a59I7y+oqQGyrPbbYlgPMUdKBOgvFLpoHbD2bCM+F75i4N0mUijy7dG5F/CUCu2qHmGGUCXBbMDkCg=="],
+ "expo": ["expo@56.0.6", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "^56.1.12", "@expo/config": "~56.0.9", "@expo/config-plugins": "~56.0.8", "@expo/devtools": "~56.0.2", "@expo/dom-webview": "~56.0.5", "@expo/fingerprint": "^0.19.3", "@expo/local-build-cache-provider": "^56.0.8", "@expo/log-box": "^56.0.12", "@expo/metro": "~56.0.0", "@expo/metro-config": "~56.0.13", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~56.0.13", "expo-asset": "~56.0.15", "expo-constants": "~56.0.16", "expo-file-system": "~56.0.7", "expo-font": "~56.0.5", "expo-keep-awake": "~56.0.3", "expo-modules-autolinking": "~56.0.14", "expo-modules-core": "~56.0.13", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" }, "peerDependencies": { "@expo/metro-runtime": "*", "react": "*", "react-dom": "*", "react-native": "*", "react-native-web": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/metro-runtime", "react-dom", "react-native-web", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-zcFa/+6hGtzCUlcrGiusvzr/PIoNBAnjj4PlAFrvbAOZcVOj6c9Mp7lRSn9XYJk8Ok6pssQWt6dP4llJlKmYRQ=="],
- "expo-application": ["expo-application@7.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q=="],
+ "expo-application": ["expo-application@56.0.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-DdGGPlMuM6cSTeKhbvh6OeLr2O/+EI5BHKYrD+Do8sJPYgLwzGrgESELfyjJCpEhFzT+TgKIdmLmWXhNUQnHiw=="],
- "expo-asset": ["expo-asset@12.0.12", "", { "dependencies": { "@expo/image-utils": "^0.8.8", "expo-constants": "~18.0.12" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ=="],
+ "expo-asset": ["expo-asset@56.0.15", "", { "dependencies": { "@expo/image-utils": "^0.10.1", "expo-constants": "~56.0.16" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-BHGi2IAOPQTcOelkUdcz1WIknfCTRjkcpYHX1azjMwgYenrVC+J5qcqJGaC8eUOWLCRtkRJWGnmFQRYtLU1nUQ=="],
- "expo-background-task": ["expo-background-task@1.0.10", "", { "dependencies": { "expo-task-manager": "~14.0.9" }, "peerDependencies": { "expo": "*" } }, "sha512-EbPnuf52Ps/RJiaSFwqKGT6TkvMChv7bI0wF42eADbH3J2EMm5y5Qvj0oFmF1CBOwc3mUhqj63o7Pl6OLkGPZQ=="],
+ "expo-audio": ["expo-audio@56.0.11", "", { "peerDependencies": { "expo": "*", "expo-asset": "*", "react": "*", "react-native": "*" } }, "sha512-naionxilr49IpEjmMqCj5gXHCSfOsgu3nZ/KXndexR05Tv6dET7dmespyZkcMrADJN07gA5hyqPUC5WqWuaFLw=="],
- "expo-blur": ["expo-blur@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-rWyE1NBRZEu9WD+X+5l7gyPRszw7n12cW3IRNAb5i6KFzaBp8cxqT5oeaphJapqURvcqhkOZn2k5EtBSbsuU7w=="],
+ "expo-background-task": ["expo-background-task@56.0.15", "", { "dependencies": { "expo-task-manager": "~56.0.15" }, "peerDependencies": { "expo": "*" } }, "sha512-ZBzLkKFmM5ZpJYl1D1kpmk6MomLbVx6LQbMX4GGLg8TSidvvtden0haIw4R5Rpkgzj3LOjvFMFli5a4kQA7VCA=="],
- "expo-brightness": ["expo-brightness@14.0.8", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-WOg3UxzkHFTKBW3XvROlrVRmnJmZLhGBGd1RdzTfrtt2/MdSzvVmCevqWh4bohkeLABh0Yc9YRo1vFgfT73DWw=="],
+ "expo-blur": ["expo-blur@56.0.3", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-KDDtrpWc2tYlm1WCPaOgBtv+YEGqe5ELheFPIgSNgHt28NQUDcfBcFsA9Us2StDh6osmSD6NbKxOt5bU6PcDbQ=="],
- "expo-build-properties": ["expo-build-properties@1.0.10", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-mFCZbrbrv0AP5RB151tAoRzwRJelqM7bCJzCkxpu+owOyH+p/rFC/q7H5q8B9EpVWj8etaIuszR+gKwohpmu1Q=="],
+ "expo-brightness": ["expo-brightness@56.0.5", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-AkCGW+Lj8I4o2+Yjs1bzjIJz44cgNXfAN+pf01uDwmA1/1JTIy8x1eISvmz6d2r/1OhdyBZxeDkACNLVMDx5zA=="],
- "expo-constants": ["expo-constants@18.0.13", "", { "dependencies": { "@expo/config": "~12.0.13", "@expo/env": "~2.0.8" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ=="],
+ "expo-build-properties": ["expo-build-properties@56.0.15", "", { "dependencies": { "@expo/schema-utils": "^56.0.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-3OlfTnBE6BIFxchjXzb0OlgDcWw19fxhIzpIZqgcgzZUVjyn4gCrQuNcsfazVVddBypwkEzOVfwArPROIk4J7g=="],
- "expo-crypto": ["expo-crypto@15.0.8", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw=="],
+ "expo-camera": ["expo-camera@56.0.7", "", { "dependencies": { "barcode-detector": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-c8z+UheidFintQyP9XLEDP43aK4PS/o9+TFLW0zEOjdqkYCBgoWq6Mw/Ps62kjBeftFY7xrp5ZLITbenNvbTaw=="],
- "expo-dev-client": ["expo-dev-client@6.0.20", "", { "dependencies": { "expo-dev-launcher": "6.0.20", "expo-dev-menu": "7.0.18", "expo-dev-menu-interface": "2.0.0", "expo-manifests": "~1.0.10", "expo-updates-interface": "~2.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA=="],
+ "expo-constants": ["expo-constants@56.0.16", "", { "dependencies": { "@expo/env": "~2.3.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA=="],
- "expo-dev-launcher": ["expo-dev-launcher@6.0.20", "", { "dependencies": { "ajv": "^8.11.0", "expo-dev-menu": "7.0.18", "expo-manifests": "~1.0.10" }, "peerDependencies": { "expo": "*" } }, "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA=="],
+ "expo-crypto": ["expo-crypto@56.0.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-fRNEhoXRXgAWBpe3/hq5X+KXTit3OZqdiAGts1YvNEUHQb+H5591mpPac0Yw+sZg9pXcrjRnzo5AxvZaENpc7g=="],
- "expo-dev-menu": ["expo-dev-menu@7.0.18", "", { "dependencies": { "expo-dev-menu-interface": "2.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA=="],
+ "expo-dev-client": ["expo-dev-client@56.0.16", "", { "dependencies": { "expo-dev-launcher": "~56.0.16", "expo-dev-menu": "~56.0.15", "expo-dev-menu-interface": "~56.0.0", "expo-manifests": "~56.0.4", "expo-updates-interface": "~56.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-mxmGA6YSP4KiMB4bREpriQ4K6EaS4tcm0eh1+LtAzgFCytq+Y4WxMfIvFe3B5kXlSpA0ohMLdAN0AUzU0xHGQg=="],
- "expo-dev-menu-interface": ["expo-dev-menu-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw=="],
+ "expo-dev-launcher": ["expo-dev-launcher@56.0.16", "", { "dependencies": { "@expo/schema-utils": "^56.0.0", "expo-dev-menu": "~56.0.15", "expo-manifests": "~56.0.4" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-3t2PCX2lCKetKL8EgRRo2tzSlGh1zcuaWuwp3V0k4/3nuM7pztyImaR6Sm3HUyarDOofAIPX1hIIxnuAfk5cnw=="],
- "expo-device": ["expo-device@8.0.10", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA=="],
+ "expo-dev-menu": ["expo-dev-menu@56.0.15", "", { "dependencies": { "expo-dev-menu-interface": "~56.0.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-FY6Y5sZkNXxPBGDgC51ZArOi8N7Y8wpXwanTClFO36IVMoVf7BBqhjW13KpDecvJONtEtaUeNIAt9C25PO8MOQ=="],
- "expo-doctor": ["expo-doctor@1.17.14", "", { "bin": { "expo-doctor": "build/index.js" } }, "sha512-+UsXFP5ZTVobDuGS5Du8aKU6O6s2sa49QOdGHdzP8UEjQKH8gPb59uw6hxEQmo6YtVboLwQd13QEdcSolBMvLw=="],
+ "expo-dev-menu-interface": ["expo-dev-menu-interface@56.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-odATx0ZL/Kis10sKSBiKiGQxAB6coSi/KQtKcMhnQVNno6FkRh5/4e5BqcEvpq2rNMTiQp4ytNAQHtdwbPXvGA=="],
- "expo-file-system": ["expo-file-system@19.0.21", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg=="],
+ "expo-device": ["expo-device@56.0.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-ucVcGPkvBrl2QHuy7XcYex2Y6BETvJ6TREutZrwLGUDnlvbpKS8KfQoNZOpvkyo5Nmm9RrasYQ0CrXmBHho2mg=="],
- "expo-font": ["expo-font@14.0.10", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q=="],
+ "expo-doctor": ["expo-doctor@1.19.7", "", { "bin": { "expo-doctor": "bin/expo-doctor.js" } }, "sha512-pzn7QtCifRlvGIQz8k7kszeYFaI5Yn81WTHlk/20tmd3jwnXxPjlcdyhFSkuRtO2v4a9gA/6aUWVBOosfffj9w=="],
- "expo-haptics": ["expo-haptics@15.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g=="],
+ "expo-file-system": ["expo-file-system@56.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA=="],
- "expo-image": ["expo-image@3.0.11", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA=="],
+ "expo-font": ["expo-font@56.0.5", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-WLoDu9hlEgPRKXJRR01HFLJ6Z2tFcORX/WFPRYBndmYc5kjQrFGH/j4BRaF3aBRPyYEAUXiUJybNLXkKCwEXQw=="],
- "expo-json-utils": ["expo-json-utils@0.15.0", "", {}, "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ=="],
+ "expo-glass-effect": ["expo-glass-effect@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-xI9rXtDwi7RW82uAlfyaXO6+k21ApWJ2tHAWYqPr/FjfmZbKsgNJ4Q0iZzGPCwboqjTGxaRZ61SZxBl8hDt5iA=="],
- "expo-keep-awake": ["expo-keep-awake@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ=="],
+ "expo-haptics": ["expo-haptics@56.0.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-ycoahZJnR9tWAVh/0mJYxbETtHRYaWjiWS8cHlP6aDGU6Q6Y8rZ5NKsuBwWw6HR2Pe30mfVFgbF2HrBR6gtYmw=="],
- "expo-linear-gradient": ["expo-linear-gradient@15.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-V2d8Wjn0VzhPHO+rrSBtcl+Fo+jUUccdlmQ6OoL9/XQB7Qk3d9lYrqKDJyccwDxmQT10JdST3Tmf2K52NLc3kw=="],
+ "expo-image": ["expo-image@56.0.9", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-FifiRehXnMul5XeUVHWv+COHFUeCAdsYf5MiCPUBlhr4pRb0sxjA4/floi/TEDpATOIw6GqxbrC4FdZBoyrJmw=="],
- "expo-linking": ["expo-linking@8.0.11", "", { "dependencies": { "expo-constants": "~18.0.12", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA=="],
+ "expo-json-utils": ["expo-json-utils@56.0.0", "", {}, "sha512-lUqyv9aIGDbYTQ5Nux2FnH2/Dz0w5uJ8Pr080eS0StXi2jr5OmuMNErpzUnpfnYOU55xKotd4AHv68PfV/ludg=="],
- "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-keep-awake": ["expo-keep-awake@56.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CLMJXtEiMKknD3Rpm8CRwE6ZJUzu2yCEmRk1sgfHAJ1zIbuEWY3dpPDubtsnuzWm+2k6Sru+yaFbYsvPWmTiBA=="],
- "expo-location": ["expo-location@19.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA=="],
+ "expo-linear-gradient": ["expo-linear-gradient@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-KUp1dNSRtuMyiExhf6FJf5YUtmw2cRaPytl10HQi7isj5Yac38udmD55T2tglNYTZlvgT5+oflpyFoH15hmOcw=="],
- "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-linking": ["expo-linking@56.0.12", "", { "dependencies": { "expo-constants": "~56.0.16", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-EJ+YoazVqlrUXMAARo1iTExpqEGjuKJDGiE/P1K+A3m5hs+2Uf8F9ucqpq9k5dizeiaV2D8B9+uLvqMHFzGGsQ=="],
- "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-localization": ["expo-localization@56.0.6", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-zzBVoUFHCVNBywcxGsspoZeIXebihOo/AnmQYE4jMv8gHCSKlLNFT+ft+0+mWcZCMs9necvUs8S8TDonAu/xBA=="],
- "expo-modules-core": ["expo-modules-core@3.0.29", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q=="],
+ "expo-location": ["expo-location@56.0.14", "", { "dependencies": { "@expo/image-utils": "^0.10.1" }, "peerDependencies": { "expo": "*" } }, "sha512-k9p6mR11o5S0R4yUs3uWLJfnSk6XIB9UIgSYiNu2goGLWb2f0sazuZ0iYhuc2p2wIsdidhpL/51ZXjtZl5JCOg=="],
- "expo-notifications": ["expo-notifications@0.32.16", "", { "dependencies": { "@expo/image-utils": "^0.8.8", "@ide/backoff": "^1.0.0", "abort-controller": "^3.0.0", "assert": "^2.0.0", "badgin": "^1.1.5", "expo-application": "~7.0.8", "expo-constants": "~18.0.13" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw=="],
+ "expo-manifests": ["expo-manifests@56.0.4", "", { "dependencies": { "expo-json-utils": "~56.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-Fokawl2UkiExIF0bqGoblRFA8lYpROVD+EpvDwSW4LgqQyPwNua1gLSgHZjdl5GsVugfRMMWE3LHaibDyX93hw=="],
- "expo-router": ["expo-router@6.0.21", "", { "dependencies": { "@expo/metro-runtime": "^6.1.2", "@expo/schema-utils": "^0.1.8", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/native": "^7.1.8", "@react-navigation/native-stack": "^7.3.16", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-server": "^1.0.5", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.1.6", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@react-navigation/drawer": "^7.5.0", "@testing-library/react-native": ">= 12.0.0", "expo": "*", "expo-constants": "^18.0.12", "expo-linking": "^8.0.11", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.3 || ~19.1.4 || ~19.2.3" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-wjTUjrnWj6gRYjaYl1kYfcRnNE4ZAQ0kz0+sQf6/mzBd/OU6pnOdD7WrdAW3pTTpm52Q8sMoeX98tNQEddg2uA=="],
+ "expo-modules-autolinking": ["expo-modules-autolinking@56.0.14", "", { "dependencies": { "@expo/require-utils": "^56.1.3", "@expo/spawn-async": "^1.8.0", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-9ugtZkheNPYDkW4DZopY1rH2BCbUICaafUEPxRgbLDR5UNRF5K3cdHMIMEt8pxZPq2+eX4wCm+6pbSvdY/DPHg=="],
- "expo-screen-orientation": ["expo-screen-orientation@9.0.8", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-qRoPi3E893o3vQHT4h1NKo51+7g2hjRSbDeg1fsSo/u2pOW5s6FCeoacLvD+xofOP33cH2MkE4ua54aWWO7Icw=="],
+ "expo-modules-core": ["expo-modules-core@56.0.13", "", { "dependencies": { "@expo/expo-modules-macros-plugin": "~0.0.9", "expo-modules-jsi": "~56.0.7", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": "^0.7.4 || ^0.8.0" }, "optionalPeers": ["react-native-worklets"] }, "sha512-3Hgpi9Q1O0XqoesQtgFY7qhfDsNA3bJtdCJotEqdE42+N8Zv/LJACbNgIyFN/XrnMDzfF5rozh0vNWaRT0/eXQ=="],
- "expo-secure-store": ["expo-secure-store@15.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw=="],
+ "expo-modules-jsi": ["expo-modules-jsi@56.0.7", "", { "peerDependencies": { "react-native": "*" } }, "sha512-iBAj4Xeh/8HT201VVxFlmf+VBfmtQV1ZUoJdLQQENm0+j9gnD2QswZLJyNo3CmNNXl46esJeLR5lpGpYZts/zA=="],
- "expo-server": ["expo-server@1.0.5", "", {}, "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA=="],
+ "expo-notifications": ["expo-notifications@56.0.14", "", { "dependencies": { "@expo/image-utils": "^0.10.1", "abort-controller": "^3.0.0", "badgin": "^1.1.5", "expo-application": "~56.0.3", "expo-constants": "~56.0.16" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-A+BDJYyBIkC17Bfqlrbf9A80npjOyoTbaSCydP2agfhVv+Ld7DuOYOJSApBmtzBZM0LvdUVX/pdrwjEp1ixmaw=="],
- "expo-sharing": ["expo-sharing@14.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-A1pPr2iBrxypFDCWVAESk532HK+db7MFXbvO2sCV9ienaFXAk7lIBm6bkqgE6vzRd9O3RGdEGzYx80cYlc089Q=="],
+ "expo-router": ["expo-router@56.2.7", "", { "dependencies": { "@expo/log-box": "^56.0.12", "@expo/metro-runtime": "^56.0.13", "@expo/schema-utils": "^56.0.0", "@expo/ui": "^56.0.14", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "client-only": "^0.0.1", "color": "^4.2.3", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^56.0.4", "expo-server": "^56.0.4", "expo-symbols": "^56.0.5", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-is": "^19.1.0", "react-native-drawer-layout": "^4.2.2", "react-native-screens": "^4.25.2", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "vaul": "^1.1.2" }, "peerDependencies": { "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^56.0.16", "expo-linking": "^56.0.12", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-T7MSugHfj6XDrVJG8dCkP5EEAWeCkPrkkxqKCqCRokXmBKTAiRGXsmPsgHzOXhr/5MxGDJXhj5ON19uWoCevDA=="],
- "expo-splash-screen": ["expo-splash-screen@31.0.13", "", { "dependencies": { "@expo/prebuild-config": "^54.0.8" }, "peerDependencies": { "expo": "*" } }, "sha512-1epJLC1cDlwwj089R2h8cxaU5uk4ONVAC+vzGiTZH4YARQhL4Stlz1MbR6yAS173GMosvkE6CAeihR7oIbCkDA=="],
+ "expo-screen-orientation": ["expo-screen-orientation@56.0.5", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-Puf4L/cgM8z45Z2fwZzJtlVGSk0ZM/l3gBqXm50bKTACmUk8P8fr7HVbDfs8reyoZuEKKFZJ0VlnKo5i6cSotQ=="],
- "expo-status-bar": ["expo-status-bar@3.0.9", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw=="],
+ "expo-secure-store": ["expo-secure-store@56.0.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-hjEi/gmpdFFJ9lYbdp3k3p/WchV7Gi0Qt8jt/m/0WJadqQrskafHAlDxbZkII1cN3Yd7zp9Lvkeq3UfGhSwirQ=="],
- "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-server": ["expo-server@56.0.4", "", {}, "sha512-4dJ57KuAwDl7eQGD6aG9kTzBIftWAfHH1+6Zxy7NcPCBrKYis3/H5enGUz1asH8HHhONXfJ5BdJqfEWAEAgWxA=="],
- "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-sharing": ["expo-sharing@56.0.14", "", { "dependencies": { "@expo/config-plugins": "^56.0.8", "@expo/config-types": "^56.0.5", "@expo/plist": "^0.7.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-Hu7pm3U9vn9NFGBe5EUM6ct6wBhAc7Zgl5koOYpJnMvL6n85bkIA8sLvvxB6V+p4JRoh3TD6xXpOIr23qwsV2w=="],
- "expo-updates-interface": ["expo-updates-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg=="],
+ "expo-splash-screen": ["expo-splash-screen@56.0.10", "", { "dependencies": { "@expo/config-plugins": "~56.0.8", "@expo/image-utils": "^0.10.1", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-vDIlo8hzt9HlCZQ0kSY66v83D1WEXOJbVMeyPDfXDu9tbDdPMNUyDpi4WGJXikAjxnAKfbt5Mv5NnEbxINy+VA=="],
- "expo-web-browser": ["expo-web-browser@15.0.10", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg=="],
+ "expo-status-bar": ["expo-status-bar@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-IGs/fDfkHXofy2ZQrGiXayhFK04HB85FZXorhcEhDZEcqASKgSqpak+HwUtAaR0MeTJwWyHNF7I6VmVbbp8EcA=="],
+
+ "expo-symbols": ["expo-symbols@56.0.5", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-RIukH0Xo80C7RU8qreipL2SPy2Py+Km8JFPbCmbPQpHkM3DW9Znlmg6VfhzbtUOlO5EuNSF0lAJ3l2VJi6qYrw=="],
+
+ "expo-system-ui": ["expo-system-ui@56.0.5", "", { "dependencies": { "@react-native/normalize-colors": "0.85.3", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-n1MmnUArV4cc3gVed9fGtluPme00PE9axKVx+NHbKxHFMam5l4GcOI7PxbYKFNx8o7WA1LRD7eLW33agmZrxGg=="],
+
+ "expo-task-manager": ["expo-task-manager@56.0.15", "", { "dependencies": { "unimodules-app-loader": "~56.0.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-8vbKYocXJHv27++9AubVaEvVujTdt5Z10XddaxHAhWO60uw1Zom6yRjSAayRbZ5hNFA1c72KfA2vOETXZR9IGg=="],
+
+ "expo-updates-interface": ["expo-updates-interface@56.0.2", "", { "peerDependencies": { "expo": "*" } }, "sha512-eWTwSZ9y8vrULG2oBn2TQSSIwBGSq/TxGJ3jY6tuVS2FWH/ASRIiKs3zkUZTRoC3ZuV2alz0mUClYV7nNrFx8g=="],
+
+ "expo-web-browser": ["expo-web-browser@56.0.5", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-kaN+wcR5lHwPCH1IgrU1XyPUQvBRzdF1TMp65uAF9iUCyipqYnmrvV87eqAmrdkFFopWVgU7FcxPu1UZw+gvUQ=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
@@ -1090,13 +1034,13 @@
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
- "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+ "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="],
- "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+ "fast-xml-parser": ["fast-xml-parser@5.8.0", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", "strnum": "^2.3.0", "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg=="],
- "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="],
+ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
- "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
+ "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="],
"fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="],
@@ -1106,6 +1050,8 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+ "fetch-nodeshim": ["fetch-nodeshim@0.4.10", "", {}, "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w=="],
+
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -1120,42 +1066,32 @@
"flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="],
- "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+ "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
"fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="],
- "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
-
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
- "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
-
- "freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="],
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
- "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
-
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
- "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
-
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
- "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
+ "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
- "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="],
-
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
@@ -1164,12 +1100,10 @@
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
- "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
+ "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
- "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="],
-
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -1182,11 +1116,13 @@
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
- "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+ "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
- "hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
+ "hermes-compiler": ["hermes-compiler@250829098.0.10", "", {}, "sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w=="],
- "hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
+ "hermes-estree": ["hermes-estree@0.33.3", "", {}, "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg=="],
+
+ "hermes-parser": ["hermes-parser@0.33.3", "", { "dependencies": { "hermes-estree": "0.33.3" } }, "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -1194,9 +1130,9 @@
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
- "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
- "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+ "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
@@ -1204,9 +1140,9 @@
"hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
- "i18next": ["i18next@25.6.1", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw=="],
+ "i18next": ["i18next@26.3.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA=="],
- "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
@@ -1218,27 +1154,19 @@
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
- "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
-
- "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
- "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
-
"inline-style-prefixer": ["inline-style-prefixer@7.0.1", "", { "dependencies": { "css-in-js-utils": "^3.1.0" } }, "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw=="],
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
- "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
-
"is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
- "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
-
- "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+ "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
@@ -1246,22 +1174,14 @@
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
-
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
- "is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="],
-
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
- "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
-
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
- "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
-
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
@@ -1272,28 +1192,16 @@
"isomorphic-fetch": ["isomorphic-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA=="],
- "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
-
- "istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="],
-
- "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+ "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
"jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="],
- "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="],
-
"jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
- "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="],
-
"jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="],
"jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="],
- "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="],
-
- "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="],
-
"jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
"jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="],
@@ -1306,13 +1214,13 @@
"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.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=="],
+ "jotai": ["jotai@2.20.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-b5GAqgmXmXzB4WPaTH26ppk9Sl7AA9WSQX7yfdM+gJ1rFROiWcVbi97gFuN/yVCojOcbcvop2sfLL+fjxW0JVg=="],
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
- "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="],
@@ -1320,8 +1228,6 @@
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
- "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
-
"json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@@ -1334,49 +1240,49 @@
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
- "lan-network": ["lan-network@0.1.7", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ=="],
+ "lan-network": ["lan-network@0.2.1", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A=="],
- "launch-editor": ["launch-editor@2.12.0", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg=="],
+ "launch-editor": ["launch-editor@2.13.2", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg=="],
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
- "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
- "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
- "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
- "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
- "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
- "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
- "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
- "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
- "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
- "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
- "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
- "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
- "lint-staged": ["lint-staged@16.2.7", "", { "dependencies": { "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow=="],
+ "lint-staged": ["lint-staged@17.0.5", "", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.1.2" }, "optionalDependencies": { "yaml": "^2.8.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-d12yC+/e8RhBjZtaxZn71FyrgU/P5e+uAPifhCLwdosQZP/zamSdKRWDC30ocVIbzDKiFG1McHc/LUgB92GIPw=="],
- "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="],
+ "listr2": ["listr2@10.2.1", "", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
- "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
+ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
@@ -1392,6 +1298,8 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
"makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="],
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
@@ -1400,7 +1308,7 @@
"mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="],
- "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
@@ -1408,33 +1316,33 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
- "metro": ["metro@0.83.3", "", { "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.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3", "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-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q=="],
+ "metro": ["metro@0.84.4", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.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.35.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.84.4", "metro-cache": "0.84.4", "metro-cache-key": "0.84.4", "metro-config": "0.84.4", "metro-core": "0.84.4", "metro-file-map": "0.84.4", "metro-resolver": "0.84.4", "metro-runtime": "0.84.4", "metro-source-map": "0.84.4", "metro-symbolicate": "0.84.4", "metro-transform-plugins": "0.84.4", "metro-transform-worker": "0.84.4", "mime-types": "^3.0.1", "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-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA=="],
- "metro-babel-transformer": ["metro-babel-transformer@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g=="],
+ "metro-babel-transformer": ["metro-babel-transformer@0.84.4", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.84.4", "nullthrows": "^1.1.1" } }, "sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g=="],
- "metro-cache": ["metro-cache@0.83.3", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.3" } }, "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q=="],
+ "metro-cache": ["metro-cache@0.84.4", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.84.4" } }, "sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg=="],
- "metro-cache-key": ["metro-cache-key@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw=="],
+ "metro-cache-key": ["metro-cache-key@0.84.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw=="],
- "metro-config": ["metro-config@0.83.3", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.3", "metro-cache": "0.83.3", "metro-core": "0.83.3", "metro-runtime": "0.83.3", "yaml": "^2.6.1" } }, "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA=="],
+ "metro-config": ["metro-config@0.84.4", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.84.4", "metro-cache": "0.84.4", "metro-core": "0.84.4", "metro-runtime": "0.84.4", "yaml": "^2.6.1" } }, "sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q=="],
- "metro-core": ["metro-core@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.3" } }, "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw=="],
+ "metro-core": ["metro-core@0.84.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.84.4" } }, "sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ=="],
- "metro-file-map": ["metro-file-map@0.83.3", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA=="],
+ "metro-file-map": ["metro-file-map@0.84.4", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ=="],
- "metro-minify-terser": ["metro-minify-terser@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ=="],
+ "metro-minify-terser": ["metro-minify-terser@0.84.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ=="],
- "metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="],
+ "metro-resolver": ["metro-resolver@0.84.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A=="],
- "metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="],
+ "metro-runtime": ["metro-runtime@0.84.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q=="],
- "metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="],
+ "metro-source-map": ["metro-source-map@0.84.4", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.84.4", "nullthrows": "^1.1.1", "ob1": "0.84.4", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g=="],
- "metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="],
+ "metro-symbolicate": ["metro-symbolicate@0.84.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.84.4", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA=="],
- "metro-transform-plugins": ["metro-transform-plugins@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A=="],
+ "metro-transform-plugins": ["metro-transform-plugins@0.84.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow=="],
- "metro-transform-worker": ["metro-transform-worker@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-minify-terser": "0.83.3", "metro-source-map": "0.83.3", "metro-transform-plugins": "0.83.3", "nullthrows": "^1.1.1" } }, "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA=="],
+ "metro-transform-worker": ["metro-transform-worker@0.84.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.84.4", "metro-babel-transformer": "0.84.4", "metro-cache": "0.84.4", "metro-cache-key": "0.84.4", "metro-minify-terser": "0.84.4", "metro-source-map": "0.84.4", "metro-transform-plugins": "0.84.4", "nullthrows": "^1.1.1" } }, "sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
@@ -1448,43 +1356,47 @@
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
- "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
+ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
- "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
-
- "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
+ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+ "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="],
+
+ "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
+
+ "multitars": ["multitars@1.0.0", "", {}, "sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg=="],
+
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
- "nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="],
-
- "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+ "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"nativewind": ["nativewind@2.0.11", "", { "dependencies": { "@babel/generator": "^7.18.7", "@babel/helper-module-imports": "7.18.6", "@babel/types": "7.19.0", "css-mediaquery": "^0.1.2", "css-to-react-native": "^3.0.0", "micromatch": "^4.0.5", "postcss": "^8.4.12", "postcss-calc": "^8.2.4", "postcss-color-functional-notation": "^4.2.2", "postcss-css-variables": "^0.18.0", "postcss-nested": "^5.0.6", "react-is": "^18.1.0", "use-sync-external-store": "^1.1.0" }, "peerDependencies": { "tailwindcss": "~3" } }, "sha512-qCEXUwKW21RYJ33KRAJl3zXq2bCq82WoI564fI21D/TiqhfmstZOqPN53RF8qK1NDK6PGl56b2xaTxgObEePEg=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
- "nested-error-stacks": ["nested-error-stacks@2.0.1", "", {}, "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A=="],
-
"nocache": ["nocache@3.0.4", "", {}, "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
- "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="],
+ "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="],
+
+ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
- "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
+ "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="],
"node-stream-zip": ["node-stream-zip@1.15.0", "", {}, "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="],
- "node-vibrant": ["node-vibrant@4.0.3", "", { "dependencies": { "@types/node": "^18.15.3", "@vibrant/core": "^4.0.0", "@vibrant/generator-default": "^4.0.3", "@vibrant/image-browser": "^4.0.0", "@vibrant/image-node": "^4.0.0", "@vibrant/quantizer-mmcq": "^4.0.0" } }, "sha512-kzoIuJK90BH/k65Avt077JCX4Nhqz1LNc8cIOm2rnYEvFdJIYd8b3SQwU1MTpzcHtr8z8jxkl1qdaCfbP3olFg=="],
+ "node-vibrant": ["node-vibrant@4.0.4", "", { "dependencies": { "@types/node": "^18.15.3", "@vibrant/core": "^4.0.4", "@vibrant/generator-default": "^4.0.4", "@vibrant/image-browser": "^4.0.4", "@vibrant/image-node": "^4.0.4", "@vibrant/quantizer-mmcq": "^4.0.4" } }, "sha512-hA/pUXBE9TJ41G9FlTkzeqD5JdxgvvPGYZb/HNpdkaxxXUEnP36imSolZ644JuPun+lTd+FpWWtBpTYdp2noQA=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@@ -1496,7 +1408,7 @@
"nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="],
- "ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="],
+ "ob1": ["ob1@0.84.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -1504,20 +1416,14 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
- "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
-
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
- "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
-
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
- "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
-
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
@@ -1546,21 +1452,19 @@
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
- "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
+ "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
- "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
+ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
- "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
- "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
+ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
@@ -1568,13 +1472,11 @@
"pixelmatch": ["pixelmatch@4.0.2", "", { "dependencies": { "pngjs": "^3.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA=="],
- "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
+ "plist": ["plist@3.1.1", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA=="],
- "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
+ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
- "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
-
- "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+ "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="],
@@ -1594,8 +1496,6 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
- "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
-
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="],
@@ -1610,13 +1510,13 @@
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
- "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
+ "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
- "qrcode-terminal": ["qrcode-terminal@0.11.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ=="],
+ "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
- "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
+ "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
"query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="],
@@ -1626,29 +1526,27 @@
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
- "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
- "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
-
- "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
+ "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
- "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
+ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
"react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
- "react-i18next": ["react-i18next@16.5.4", "", { "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-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g=="],
+ "react-i18next": ["react-i18next@17.0.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5 || ^6" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw=="],
- "react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="],
+ "react-is": ["react-is@19.2.6", "", {}, "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw=="],
- "react-native": ["react-native@0.81.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", "@react-native/codegen": "0.81.5", "@react-native/community-cli-plugin": "0.81.5", "@react-native/gradle-plugin": "0.81.5", "@react-native/js-polyfills": "0.81.5", "@react-native/normalize-colors": "0.81.5", "@react-native/virtualized-lists": "0.81.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw=="],
+ "react-native": ["react-native-tvos@0.85.3-0", "", { "dependencies": { "@react-native-tvos/virtualized-lists": "0.85.3-0", "@react-native/assets-registry": "0.85.3", "@react-native/codegen": "0.85.3", "@react-native/community-cli-plugin": "0.85.3", "@react-native/gradle-plugin": "0.85.3", "@react-native/js-polyfills": "0.85.3", "@react-native/normalize-colors": "0.85.3", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-plugin-syntax-hermes-parser": "0.33.3", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "hermes-compiler": "250829098.0.10", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.3", "metro-source-map": "^0.84.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@react-native/jest-preset": "0.85.3", "@types/react": "^19.1.1", "react": "^19.2.3" }, "optionalPeers": ["@react-native/jest-preset", "@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-Q9gUndppXbGEiYlQ8eudkdH7rDXdY+KM74Btd5xqMvXHgo7ZXdwI1hKvStmI47KmTaDn0NOmcRl2yBwHfc5+5A=="],
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
- "react-native-bottom-tabs": ["react-native-bottom-tabs@1.1.0", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Uu1gvM3i1Hb4DjVvR/38J1QVQEs0RkPc7K6yon99HgvRWWOyLs7kjPDhUswtb8ije4pKW712skIXWJ0lgKzbyQ=="],
+ "react-native-bottom-tabs": ["react-native-bottom-tabs@1.2.0", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ScVPko86ts+m6JMNtI24MCSYJCOZc1aZkn9qwS9ly3o0ubajRWDpCzgRJfRFysi08bKrcqAXKVCHZNHvNb2PTA=="],
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
@@ -1656,63 +1554,65 @@
"react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="],
- "react-native-device-info": ["react-native-device-info@15.0.1", "", { "peerDependencies": { "react-native": "*" } }, "sha512-U5waZRXtT3l1SgZpZMlIvMKPTkFZPH8W7Ks6GrJhdH723aUIPxjVer7cRSij1mvQdOAAYFJV/9BDzlC8apG89A=="],
+ "react-native-device-info": ["react-native-device-info@15.0.2", "", { "peerDependencies": { "react-native": "*" } }, "sha512-dd71eXG2l3Cwp66IvKNadMTB8fhU3PEjyVddI97sYan+D4bgIAUmgGDhbSOFvHcGavksb2U17kiQYaDiK2WK2g=="],
"react-native-draggable-flatlist": ["react-native-draggable-flatlist@4.0.3", "", { "dependencies": { "@babel/preset-typescript": "^7.17.12" }, "peerDependencies": { "react-native": ">=0.64.0", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=2.8.0" } }, "sha512-2F4x5BFieWdGq9SetD2nSAR7s7oQCSgNllYgERRXXtNfSOuAGAVbDb/3H3lP0y5f7rEyNwabKorZAD/SyyNbDw=="],
- "react-native-edge-to-edge": ["react-native-edge-to-edge@1.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ=="],
+ "react-native-drawer-layout": ["react-native-drawer-layout@4.2.4", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-l1Le5HcVidobnJm8xqFZo46Rs8FDHdxbTZhkjxpNSRgU+QMoQXilOfzTHAeNjEGiKVGgIs9cW3ctXeHqgp5jJg=="],
- "react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
+ "react-native-edge-to-edge": ["react-native-edge-to-edge@1.8.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-bhvsKqeX9PGkY9wBUk9vni/tJNJdKtLPbs/j3e/3CdV4JmUWfTXYYoL+4Hc8Wmej+5eJxkc8KOFa454ruFWBCA=="],
+
+ "react-native-gesture-handler": ["react-native-gesture-handler@2.31.2", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A=="],
"react-native-glass-effect-view": ["react-native-glass-effect-view@1.0.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ABYG0oIiqbXsxe2R/cMhNgDn3YgwDLz/2TIN2XOxQopXC+MiGsG9C32VYQvO2sYehcu5JmI3h3EzwLwl6lJhhA=="],
"react-native-google-cast": ["react-native-google-cast@4.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/HvIKAaWHtG6aTNCxrNrqA2ftWGkfH0M/2iN+28pdGUXpKmueb33mgL1m8D4zzwEODQMcmpfoCsym1IwDvugBQ=="],
- "react-native-image-colors": ["react-native-image-colors@2.5.0", "", { "dependencies": { "node-vibrant": "^4.0.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-3zSDgNj5HaZ0PDWaXkc4BpWpZRM5N4gBsoPC7DBfM/+op69Yvwbc0S1T7CnxBWbvShtOvRE+b2BUBadVn+6z/g=="],
+ "react-native-image-colors": ["react-native-image-colors@2.6.0", "", { "dependencies": { "node-vibrant": "^4.0.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-MbBPmRpp2yy8h5W7KUreByP96pey0J9habHaRSN/67O0hlR/5Izpt370BNHQVQogfHrRXfV4d8n6ZLn/2ga7Bg=="],
"react-native-ios-context-menu": ["react-native-ios-context-menu@3.2.1", "", { "dependencies": { "@dominicstop/ts-event-emitter": "^1.1.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-ios-utilities": "*" } }, "sha512-OBQbb3I/VUx2wQgz4cqN614kt3nJ+qx5wxEdtGN1Aj4nYYL1orp7VLFkV6axof6DgOyv0YD6af2RUTok6a2xDQ=="],
"react-native-ios-utilities": ["react-native-ios-utilities@5.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-RTw1Gk8rQhBL43+U80I+Nu8T7mLTNkj5RaG8vTs3ETEDqphS3L0Mrzk79RX0Jmm64HMad70GXHctXFlW1n0V8w=="],
- "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
+ "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="],
"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.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@8.0.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-pGOne2o0y0HOQLrlTLcGgOE48uJlqSZHRRwdW8nL6JJozMkPGJYi/G9e0EsJoWFpXYONjiDgr8IwxC4F6/r7Lg=="],
- "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-qrcode-svg": ["react-native-qrcode-svg@6.3.21", "", { "dependencies": { "prop-types": "^15.8.0", "qrcode": "^1.5.4", "text-encoding": "^0.7.0" }, "peerDependencies": { "react": "*", "react-native": ">=0.63.4", "react-native-svg": ">=14.0.0" } }, "sha512-6vcj4rcdpWedvphDR+NSJcudJykNuLgNGFwm2p4xYjR8RdyTzlrELKI5LkO4ANS9cQUbqsfkpippPv64Q2tUtA=="],
+
+ "react-native-reanimated": ["react-native-reanimated@4.3.1", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.81 - 0.85", "react-native-worklets": "0.8.x" } }, "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q=="],
"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.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ=="],
- "react-native-screens": ["react-native-screens@4.18.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ=="],
+ "react-native-screens": ["react-native-screens@4.25.2", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": ">=0.82.0" } }, "sha512-1Nj1fusFd+rIMKU/qC9yGKVG+3ofh11d3OdBQKL1iVvQfKvcB8vhvTGQf2TkfxW3bamxN+hCZIXmNuU0mRkyDg=="],
- "react-native-svg": ["react-native-svg@15.12.1", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g=="],
-
- "react-native-tab-view": ["react-native-tab-view@4.2.0", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-TUbh7Yr0tE/99t1pJQLbQ+4/Px67xkT7/r3AhfV+93Q3WoUira0Lx7yuKUP2C118doqxub8NCLERwcqsHr29nQ=="],
+ "react-native-svg": ["react-native-svg@15.15.4", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A=="],
"react-native-text-ticker": ["react-native-text-ticker@1.15.0", "", {}, "sha512-d/uK+PIOhsYMy1r8h825iq/nADiHsabz3WMbRJSnkpQYn+K9aykUAXRRhu8ZbTAzk4CgnUWajJEFxS5ZDygsdg=="],
- "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#003afd0", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-003afd0"],
+ "react-native-track-player": ["react-native-track-player@github:lovegaoshi/react-native-track-player#33a3ecd", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-windows": "*", "shaka-player": "^4.7.9" }, "optionalPeers": ["react-native-windows", "shaka-player"] }, "lovegaoshi-react-native-track-player-33a3ecd"],
"react-native-udp": ["react-native-udp@4.1.7", "", { "dependencies": { "buffer": "^5.6.0", "events": "^3.1.0" } }, "sha512-NUE3zewu61NCdSsLlj+l0ad6qojcVEZPT4hVG/x6DU9U4iCzwtfZSASh9vm7teAcVzLkdD+cO3411LHshAi/wA=="],
"react-native-url-polyfill": ["react-native-url-polyfill@2.0.0", "", { "dependencies": { "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "react-native": "*" } }, "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA=="],
- "react-native-uuid": ["react-native-uuid@2.0.3", "", {}, "sha512-f/YfIS2f5UB+gut7t/9BKGSCYbRA9/74A5R1MDp+FLYsuS+OSWoiM/D8Jko6OJB6Jcu3v6ONuddvZKHdIGpeiw=="],
+ "react-native-uuid": ["react-native-uuid@2.0.4", "", {}, "sha512-LSJNeh559qC17fgVPBsWuTSW/OygFp2dwTcf94IQBLYft5FzIQS9pCsuT36OPvyvDOMb6yiGr6TafaJDnz9PPQ=="],
"react-native-volume-manager": ["react-native-volume-manager@2.0.8", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-aZM47/mYkdQ4CbXpKYO6Ajiczv7fxbQXZ9c0H8gRuQUaS3OCz/MZABer6o9aDWq0KMNsQ7q7GVFLRPnSSeeMmw=="],
"react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],
- "react-native-worklets": ["react-native-worklets@0.5.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w=="],
+ "react-native-worklets": ["react-native-worklets@0.8.3", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.3" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.81 - 0.85" } }, "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
- "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
+ "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@@ -1728,6 +1628,8 @@
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
"regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="],
"regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="],
@@ -1738,25 +1640,17 @@
"regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="],
- "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="],
+ "regjsparser": ["regjsparser@0.13.1", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
- "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
-
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
- "requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="],
-
- "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
+ "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
- "resolve-global": ["resolve-global@1.0.0", "", { "dependencies": { "global-dirs": "^0.1.1" } }, "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw=="],
-
- "resolve-workspace-root": ["resolve-workspace-root@2.0.0", "", {}, "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw=="],
-
- "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
+ "resolve-workspace-root": ["resolve-workspace-root@2.0.1", "", {}, "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w=="],
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
@@ -1764,29 +1658,25 @@
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
- "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
-
"rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
- "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
-
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
- "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
+ "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="],
+ "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="],
- "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
+ "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
@@ -1798,7 +1688,7 @@
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
- "sf-symbols-typescript": ["sf-symbols-typescript@2.1.0", "", {}, "sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A=="],
+ "sf-symbols-typescript": ["sf-symbols-typescript@2.2.0", "", {}, "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw=="],
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
@@ -1806,11 +1696,11 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
- "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
+ "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
- "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+ "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
@@ -1826,9 +1716,9 @@
"slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="],
- "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
+ "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
- "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
+ "slugify": ["slugify@1.6.9", "", {}, "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg=="],
"sonner-native": ["sonner-native@0.21.2", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.10.1", "react-native-safe-area-context": ">=4.10.5", "react-native-screens": ">=3.31.1", "react-native-svg": ">=15.6.0" } }, "sha512-LnGPmfgzrNIwcc+FvcLJqx8aH1dEHePRzvNR8aIR4kl9spySRkXK160GmQIazjfm6mSMlPqZwRa5eycvrzg/eQ=="],
@@ -1840,15 +1730,13 @@
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
- "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
-
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
"stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="],
- "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="],
@@ -1860,19 +1748,15 @@
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
- "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
- "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
- "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
+ "strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
@@ -1880,7 +1764,7 @@
"styleq": ["styleq@0.1.3", "", {}, "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA=="],
- "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
+ "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -1888,17 +1772,15 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
+
"tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
- "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="],
-
- "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="],
-
"terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="],
- "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="],
+ "terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="],
- "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="],
+ "text-encoding": ["text-encoding@0.7.0", "", {}, "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
@@ -1910,9 +1792,11 @@
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
- "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+ "tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="],
- "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
+ "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
+ "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="],
"tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="],
@@ -1924,25 +1808,23 @@
"token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
+ "toqr": ["toqr@0.1.1", "", {}, "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA=="],
+
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
- "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
-
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
- "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
+ "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="],
- "undici": ["undici@6.22.0", "", {}, "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw=="],
-
- "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+ "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
@@ -1952,19 +1834,17 @@
"unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="],
- "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=="],
+ "unimodules-app-loader": ["unimodules-app-loader@56.0.1", "", {}, "sha512-Z801jeBOQMUF/ExklxT1BqhEV/oF2/Bii7PFYAj/8Sauxl7oKvZbf70peRzzAU0mG7UQ3yU/UO/EpD1JyJ2WcA=="],
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
- "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
- "use-debounce": ["use-debounce@10.0.6", "", { "peerDependencies": { "react": "*" } }, "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg=="],
+ "use-debounce": ["use-debounce@10.1.1", "", { "peerDependencies": { "react": "*" } }, "sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ=="],
"use-latest-callback": ["use-latest-callback@0.2.6", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg=="],
@@ -1974,8 +1854,6 @@
"utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
- "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
-
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
@@ -2004,28 +1882,22 @@
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+ "whatwg-url-minimum": ["whatwg-url-minimum@0.1.2", "", {}, "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A=="],
+
"whatwg-url-without-unicode": ["whatwg-url-without-unicode@8.0.0-3", "", { "dependencies": { "buffer": "^5.4.3", "punycode": "^2.1.1", "webidl-conversions": "^5.0.0" } }, "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
- "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
+ "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="],
- "wonka": ["wonka@6.3.5", "", {}, "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw=="],
-
- "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
-
- "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
-
- "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
-
- "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="],
-
- "ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="],
+ "ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="],
"xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="],
+ "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
+
"xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="],
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
@@ -2034,7 +1906,7 @@
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
- "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
+ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -2042,109 +1914,43 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
- "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
+ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
- "@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=="],
+ "zxing-wasm": ["zxing-wasm@3.0.3", "", { "dependencies": { "@types/emscripten": "^1.41.5", "type-fest": "^5.6.0" } }, "sha512-DdOn/G5F+qvZELWeO5ZFFwcN611TfMybxPV0LUUoutUmiH2t47MZSB7gLV9O9YLhvudBdnzQNAoFOu4Xz8eOrQ=="],
- "@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-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
- "@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/plugin-transform-async-to-generator/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
- "@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/plugin-transform-react-jsx/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
- "@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/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-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/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/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=="],
+ "@babel/plugin-transform-runtime/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
"@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.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@expo/cli/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="],
- "@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="],
-
- "@expo/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@expo/cli/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"@expo/cli/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
- "@expo/cli/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+ "@expo/cli/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
- "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
+ "@expo/cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@expo/config/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
- "@expo/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/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
- "@expo/config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
-
- "@expo/config/sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
+ "@expo/config/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"@expo/config-plugins/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
- "@expo/config-plugins/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/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
- "@expo/config-plugins/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
-
- "@expo/config-plugins/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
+ "@expo/config-plugins/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -2152,49 +1958,27 @@
"@expo/fingerprint/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
- "@expo/fingerprint/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/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
- "@expo/fingerprint/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@expo/fingerprint/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"@expo/image-utils/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
- "@expo/image-utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@expo/image-utils/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "@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/@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=="],
"@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/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
+ "@expo/metro-config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="],
- "@expo/prebuild-config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@expo/prebuild-config/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "@expo/xcpretty/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
+ "@expo/require-utils/@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=="],
- "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
-
- "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
- "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
-
- "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
-
- "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
-
- "@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/types/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@jimp/png/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
@@ -2204,89 +1988,51 @@
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
- "@react-native-community/cli/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@react-native-community/cli/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "@react-native-community/cli-doctor/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@react-native-community/cli-doctor/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"@react-native-community/cli-server-api/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
- "@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "@react-native-community/cli-server-api/ws": ["ws@6.2.4", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw=="],
- "@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-community/cli-tools/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "@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/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=="],
- "@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.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=="],
- "@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/community-cli-plugin/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "@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/metro-babel-transformer/@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=="],
- "@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=="],
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
- "@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=="],
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
- "@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.2" } }, "sha512-8DRb0O82Br0IW77cNgKMLYWUkx48lWxUkvNUxVISyMkcNwE/9ywf1MYQUE88HaKwSrqne6kFgCSA/UWZoUT0Iw=="],
-
- "@react-native/community-cli-plugin/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
-
- "@react-navigation/bottom-tabs/@react-navigation/elements": ["@react-navigation/elements@2.8.1", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ=="],
-
- "@react-navigation/bottom-tabs/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
-
- "@react-navigation/core/react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
-
- "@react-navigation/elements/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
-
- "@react-navigation/material-top-tabs/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/@react-navigation/elements": ["@react-navigation/elements@2.8.1", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.19", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ=="],
-
- "@react-navigation/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.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
-
- "@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=="],
+ "@testing-library/dom/pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
- "ansi-fragments/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
-
"ansi-fragments/slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="],
"ansi-fragments/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
- "babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
+ "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
- "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-preset-expo/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
- "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=="],
-
- "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
-
- "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
+ "brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
- "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
+ "chrome-launcher/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
+
+ "chromium-edge-launcher/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
+
+ "cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -2298,13 +2044,11 @@
"error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
- "expo-build-properties/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
-
- "expo-manifests/@expo/config": ["@expo/config@12.0.11", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.3", "@expo/config-types": "^54.0.9", "@expo/json-file": "^10.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "~3.35.1" } }, "sha512-bGKNCbHirwgFlcOJHXpsAStQvM0nU3cmiobK0o07UkTfcUxl9q9lOQQh2eoMGqpm6Vs1IcwBpYye6thC3Nri/w=="],
+ "expo-build-properties/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
- "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
+ "expo-router/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -2322,8 +2066,6 @@
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
- "glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
-
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -2332,87 +2074,67 @@
"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-util/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
+
+ "jest-util/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+ "jest-worker/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
+
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
- "lint-staged/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
-
"log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
- "log-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
+ "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
+
+ "log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+
+ "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"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.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=="],
- "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/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"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.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
- "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/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
- "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/@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=="],
- "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.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
- "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-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
- "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.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=="],
- "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-worker/@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=="],
- "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=="],
+ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"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=="],
- "node-vibrant/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
+ "npm-package-arg/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "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=="],
+ "parse-png/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
"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.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
+ "path-scurry/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
+
+ "pixelmatch/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
+
+ "plist/@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
"postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -2422,34 +2144,30 @@
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
- "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
-
- "react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
+ "qrcode/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=="],
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
- "react-native/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
+ "react-native/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
- "react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "react-native-drawer-layout/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
- "react-native-reanimated/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "react-native-reanimated/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="],
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
- "react-native-worklets/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+ "react-native-worklets/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
- "requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="],
+ "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
- "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
-
"simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="],
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -2462,173 +2180,27 @@
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
- "sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
-
"tailwindcss/postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
- "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
-
"terminal-link/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
- "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+ "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
- "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+ "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
- "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+ "wrap-ansi/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
- "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
- "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+ "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"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/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/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=="],
+ "zxing-wasm/type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="],
"@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=="],
@@ -2640,34 +2212,6 @@
"@expo/cli/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
- "@expo/config-plugins/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
-
- "@expo/config/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
-
- "@expo/config/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
-
- "@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/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/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="],
@@ -2676,147 +2220,11 @@
"@expo/package-manager/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
- "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
-
- "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
-
- "@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=="],
+ "@jest/types/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"@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/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
-
- "@react-native/community-cli-plugin/metro/metro-babel-transformer": ["metro-babel-transformer@0.83.2", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-rirY1QMFlA1uxH3ZiNauBninwTioOgwChnRdDcbB4tgRZ+bGX9DiXoh9QdpppiaVKXdJsII932OwWXGGV4+Nlw=="],
-
- "@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.83.2", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.2" } }, "sha512-Z43IodutUZeIS7OTH+yQFjc59QlFJ6s5OvM8p2AP9alr0+F8UKr8ADzFzoGKoHefZSKGa4bJx7MZJLF6GwPDHQ=="],
-
- "@react-native/community-cli-plugin/metro/metro-cache-key": ["metro-cache-key@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-3EMG/GkGKYoTaf5RqguGLSWRqGTwO7NQ0qXKmNBjr0y6qD9s3VBXYlwB+MszGtmOKsqE9q3FPrE5Nd9Ipv7rZw=="],
-
- "@react-native/community-cli-plugin/metro/metro-file-map": ["metro-file-map@0.83.2", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-cMSWnEqZrp/dzZIEd7DEDdk72PXz6w5NOKriJoDN9p1TDQ5nAYrY2lHi8d6mwbcGLoSlWmpPyny9HZYFfPWcGQ=="],
-
- "@react-native/community-cli-plugin/metro/metro-resolver": ["metro-resolver@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Yf5mjyuiRE/Y+KvqfsZxrbHDA15NZxyfg8pIk0qg47LfAJhpMVEX+36e6ZRBq7KVBqy6VDX5Sq55iHGM4xSm7Q=="],
-
- "@react-native/community-cli-plugin/metro/metro-runtime": ["metro-runtime@0.83.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-nnsPtgRvFbNKwemqs0FuyFDzXLl+ezuFsUXDbX8o0SXOfsOPijqiQrf3kuafO1Zx1aUWf4NOrKJMAQP5EEHg9A=="],
-
- "@react-native/community-cli-plugin/metro/metro-source-map": ["metro-source-map@0.83.2", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.2", "nullthrows": "^1.1.1", "ob1": "0.83.2", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-5FL/6BSQvshIKjXOennt9upFngq2lFvDakZn5LfauIVq8+L4sxXewIlSTcxAtzbtjAIaXeOSVMtCJ5DdfCt9AA=="],
-
- "@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.2", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-KoU9BLwxxED6n33KYuQQuc5bXkIxF3fSwlc3ouxrrdLWwhu64muYZNQrukkWzhVKRNFIXW7X2iM8JXpi2heIPw=="],
-
- "@react-native/community-cli-plugin/metro/metro-transform-plugins": ["metro-transform-plugins@0.83.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-5WlW25WKPkiJk2yA9d8bMuZrgW7vfA4f4MBb9ZeHbTB3eIAoNN8vS8NENgG/X/90vpTB06X66OBvxhT3nHwP6A=="],
-
- "@react-native/community-cli-plugin/metro/metro-transform-worker": ["metro-transform-worker@0.83.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.2", "metro-babel-transformer": "0.83.2", "metro-cache": "0.83.2", "metro-cache-key": "0.83.2", "metro-minify-terser": "0.83.2", "metro-source-map": "0.83.2", "metro-transform-plugins": "0.83.2", "nullthrows": "^1.1.1" } }, "sha512-G5DsIg+cMZ2KNfrdLnWMvtppb3+Rp1GMyj7Bvd9GgYc/8gRmvq1XVEF9XuO87Shhb03kFhGqMTgZerz3hZ1v4Q=="],
-
- "@react-native/community-cli-plugin/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=="],
-
- "@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.2", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.2" } }, "sha512-Z43IodutUZeIS7OTH+yQFjc59QlFJ6s5OvM8p2AP9alr0+F8UKr8ADzFzoGKoHefZSKGa4bJx7MZJLF6GwPDHQ=="],
-
- "@react-native/community-cli-plugin/metro-config/metro-runtime": ["metro-runtime@0.83.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-nnsPtgRvFbNKwemqs0FuyFDzXLl+ezuFsUXDbX8o0SXOfsOPijqiQrf3kuafO1Zx1aUWf4NOrKJMAQP5EEHg9A=="],
-
- "@react-native/community-cli-plugin/metro-core/metro-resolver": ["metro-resolver@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Yf5mjyuiRE/Y+KvqfsZxrbHDA15NZxyfg8pIk0qg47LfAJhpMVEX+36e6ZRBq7KVBqy6VDX5Sq55iHGM4xSm7Q=="],
-
- "@react-navigation/bottom-tabs/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
- "@react-navigation/bottom-tabs/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
-
- "@react-navigation/elements/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
- "@react-navigation/elements/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
-
- "@react-navigation/material-top-tabs/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
- "@react-navigation/material-top-tabs/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
-
- "@react-navigation/native-stack/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
- "@react-navigation/native-stack/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
+ "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"ansi-fragments/slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
@@ -2824,19 +2232,13 @@
"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=="],
-
"chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
- "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
+ "chrome-launcher/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+
+ "chromium-edge-launcher/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+
+ "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -2844,48 +2246,30 @@
"connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
- "expo-manifests/@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
+ "expo-router/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
- "expo-manifests/@expo/config/@expo/config-plugins": ["@expo/config-plugins@54.0.3", "", { "dependencies": { "@expo/config-types": "^54.0.9", "@expo/json-file": "~10.0.7", "@expo/plist": "^0.4.7", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-tBIUZIxLQfCu5jmqTO+UOeeDUGIB0BbK6xTMkPRObAXRQeTLPPfokZRCo818d2owd+Bcmq1wBaDz0VY3g+glfw=="],
-
- "expo-manifests/@expo/config/@expo/config-types": ["@expo/config-types@54.0.9", "", {}, "sha512-Llf4jwcrAnrxgE5WCdAOxtMf8FGwS4Sk0SSgI0NnIaSyCnmOCAm80GPFvsK778Oj19Ub4tSyzdqufPyeQPksWw=="],
-
- "expo-manifests/@expo/config/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
-
- "expo-manifests/@expo/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-manifests/@expo/config/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
-
- "expo-manifests/@expo/config/sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
+ "expo-router/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
- "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+ "jest-util/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
- "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=="],
+ "jest-worker/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"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/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
+
"log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
"logkitty/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"logkitty/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
@@ -2894,154 +2278,42 @@
"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/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="],
- "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-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
- "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/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
- "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/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="],
- "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/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
- "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-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=="],
-
- "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=="],
-
- "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.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+ "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
+
+ "qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
+
+ "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
+
+ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
+
+ "react-native-drawer-layout/color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "react-native-drawer-layout/color/color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
+
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
- "serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
-
- "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
-
- "serve-static/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
-
- "sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
"terminal-link/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
- "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
-
- "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
- "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+ "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
- "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
-
- "@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/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -3054,10 +2326,6 @@
"@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/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -3068,109 +2336,39 @@
"@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
- "@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/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-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.2", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-zvIxnh7U0JQ7vT4quasKsijId3dOAWgq+ip2jF/8TMrPUqQabGrs04L2dd0haQJ+PA+d4VvK/bPOY8X/vL2PWw=="],
-
- "@react-navigation/bottom-tabs/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@react-navigation/bottom-tabs/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@react-navigation/elements/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@react-navigation/elements/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@react-navigation/material-top-tabs/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@react-navigation/material-top-tabs/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@react-navigation/native-stack/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@react-navigation/native-stack/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
"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=="],
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
- "expo-manifests/@expo/config/@expo/config-plugins/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
+ "expo-router/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
- "expo-manifests/@expo/config/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
-
- "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=="],
+ "expo-router/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"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/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
"logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"logkitty/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"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=="],
+ "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
- "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=="],
+ "qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
- "metro-transform-plugins/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
+ "qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
- "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=="],
+ "react-native-drawer-layout/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
- "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=="],
-
- "sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
- "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
+ "react-native-drawer-layout/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"@expo/cli/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
@@ -3186,12 +2384,6 @@
"@expo/package-manager/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="],
- "@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=="],
"cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -3200,6 +2392,10 @@
"logkitty/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
+ "qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
+
"@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"@expo/cli/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="],
@@ -3212,6 +2408,12 @@
"logkitty/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
+ "qrcode/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
+
"logkitty/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "qrcode/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
}
}
diff --git a/components/Badge.tsx b/components/Badge.tsx
index b33fff2b6..4c3bcc821 100644
--- a/components/Badge.tsx
+++ b/components/Badge.tsx
@@ -1,5 +1,7 @@
+import { BlurView } from "expo-blur";
import { Platform, StyleSheet, View, type ViewProps } from "react-native";
import { GlassEffectView } from "react-native-glass-effect-view";
+import { useScaledTVTypography } from "@/constants/TVTypography";
import { Text } from "./common/Text";
interface Props extends ViewProps {
@@ -14,6 +16,8 @@ export const Badge: React.FC = ({
variant = "purple",
...props
}) => {
+ const typography = useScaledTVTypography();
+
const content = (
{iconLeft && {iconLeft}}
@@ -28,7 +32,7 @@ export const Badge: React.FC = ({
);
- if (Platform.OS === "ios") {
+ if (Platform.OS === "ios" && !Platform.isTV) {
return (
@@ -38,21 +42,70 @@ export const Badge: React.FC = ({
);
}
+ // On TV, use BlurView for consistent styling
+ if (Platform.isTV) {
+ return (
+
+
+ {iconLeft && {iconLeft}}
+
+ {text}
+
+
+
+ );
+ }
+
return (
- {iconLeft && {iconLeft}}
+ {iconLeft && {iconLeft}}
{text}
diff --git a/components/Button.tsx b/components/Button.tsx
index 03df89674..1471a5174 100644
--- a/components/Button.tsx
+++ b/components/Button.tsx
@@ -15,6 +15,7 @@ import {
View,
} from "react-native";
import { useHaptic } from "@/hooks/useHaptic";
+import { scaleSize } from "@/utils/scaleSize";
import { Loader } from "./Loader";
const getColorClasses = (
@@ -122,7 +123,7 @@ export const Button: React.FC> = ({
onPress={onPress}
onFocus={() => {
setFocused(true);
- animateTo(1.08);
+ animateTo(1.03);
}}
onBlur={() => {
setFocused(false);
@@ -132,19 +133,29 @@ export const Button: React.FC> = ({
-
+
{children}
diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx
index e50b4efca..b1f759b57 100644
--- a/components/DownloadItem.tsx
+++ b/components/DownloadItem.tsx
@@ -9,6 +9,7 @@ import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
+import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { type Href } from "expo-router";
import { t } from "i18next";
import { useAtom } from "jotai";
@@ -73,12 +74,16 @@ export const DownloadItems: React.FC = ({
SelectedOptions | undefined
>(undefined);
+ const playSettingsOptions = useMemo(
+ () => ({ applyLanguagePreferences: true }),
+ [],
+ );
const {
defaultAudioIndex,
defaultBitrate,
defaultMediaSource,
defaultSubtitleIndex,
- } = useDefaultPlaySettings(items[0], settings);
+ } = useDefaultPlaySettings(items[0], settings, playSettingsOptions);
const userCanDownload = useMemo(
() => user?.Policy?.EnableContentDownloading,
@@ -195,9 +200,30 @@ export const DownloadItems: React.FC = ({
);
}
const downloadDetailsPromises = items.map(async (item) => {
+ // Ensure the snapshot we store offline carries the Chapters array.
+ // Page-level fetches sometimes use a fields filter that omits it; the
+ // offline player would then render no chapter ticks / list.
+ let itemForDownload = item;
+ if (!itemForDownload.Chapters && itemForDownload.Id) {
+ try {
+ const enriched = await getUserLibraryApi(api).getItem({
+ itemId: itemForDownload.Id,
+ userId: user.Id!,
+ });
+ if (enriched.data) {
+ itemForDownload = enriched.data;
+ }
+ } catch (e) {
+ console.warn(
+ "[DownloadItem] failed to refresh item for Chapters, falling back to original",
+ e,
+ );
+ }
+ }
+
const { mediaSource, audioIndex, subtitleIndex } =
itemsNotDownloaded.length > 1
- ? getDefaultPlaySettings(item, settings!)
+ ? getDefaultPlaySettings(itemForDownload, settings!)
: {
mediaSource: selectedOptions?.mediaSource,
audioIndex: selectedOptions?.audioIndex,
@@ -206,7 +232,7 @@ export const DownloadItems: React.FC = ({
const downloadDetails = await getDownloadUrl({
api,
- item,
+ item: itemForDownload,
userId: user.Id!,
mediaSource: mediaSource!,
audioStreamIndex: audioIndex ?? -1,
@@ -218,7 +244,7 @@ export const DownloadItems: React.FC = ({
return {
url: downloadDetails?.url,
- item,
+ item: itemForDownload,
mediaSource: downloadDetails?.mediaSource,
};
});
diff --git a/components/GenreTags.tsx b/components/GenreTags.tsx
index 030d554cf..29f1cb303 100644
--- a/components/GenreTags.tsx
+++ b/components/GenreTags.tsx
@@ -1,4 +1,5 @@
// GenreTags.tsx
+import { BlurView } from "expo-blur";
import type React from "react";
import {
Platform,
@@ -9,6 +10,7 @@ import {
type ViewProps,
} from "react-native";
import { GlassEffectView } from "react-native-glass-effect-view";
+import { useScaledTVTypography } from "@/constants/TVTypography";
import { Text } from "./common/Text";
interface TagProps {
@@ -23,7 +25,10 @@ export const Tag: React.FC<
textStyle?: StyleProp;
} & ViewProps
> = ({ text, textClass, textStyle, ...props }) => {
- if (Platform.OS === "ios") {
+ // Hook must be called at the top level, before any conditional returns
+ const typography = useScaledTVTypography();
+
+ if (Platform.OS === "ios" && !Platform.isTV) {
return (
@@ -40,6 +45,32 @@ export const Tag: React.FC<
);
}
+ // TV-specific styling with blur background
+ if (Platform.isTV) {
+ return (
+
+
+
+ {text}
+
+
+
+ );
+ }
+
return (
@@ -66,7 +97,8 @@ export const Tags: React.FC<
return (
{tags.map((tag, idx) => (
diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx
index 5195e993b..97b2f4293 100644
--- a/components/ItemContent.tsx
+++ b/components/ItemContent.tsx
@@ -15,7 +15,6 @@ import { ItemPeopleSections } from "@/components/item/ItemPeopleSections";
import { MediaSourceButton } from "@/components/MediaSourceButton";
import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage";
-// const PlayButton = !Platform.isTV ? require("@/components/PlayButton") : null;
import { PlayButton } from "@/components/PlayButton";
import { PlayedStatus } from "@/components/PlayedStatus";
import { SimilarItems } from "@/components/SimilarItems";
@@ -37,6 +36,9 @@ import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
import { PlayInRemoteSessionButton } from "./PlayInRemoteSession";
const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
+const ItemContentTV = Platform.isTV
+ ? require("./ItemContent.tv").ItemContentTV
+ : null;
export type SelectedOptions = {
bitrate: Bitrate;
@@ -46,242 +48,267 @@ export type SelectedOptions = {
};
interface ItemContentProps {
- item: BaseItemDto;
+ item?: BaseItemDto | null;
itemWithSources?: BaseItemDto | null;
+ isLoading?: boolean;
}
-export const ItemContent: React.FC = React.memo(
- ({ item, itemWithSources }) => {
- const [api] = useAtom(apiAtom);
- const isOffline = useOfflineMode();
- const { getDownloadedItemById } = useDownload();
- const downloadedItem =
- isOffline && item.Id ? getDownloadedItemById(item.Id) : null;
- const { settings } = useSettings();
- const { orientation } = useOrientation();
- const navigation = useNavigation();
- const insets = useSafeAreaInsets();
- const [user] = useAtom(userAtom);
+// Mobile-specific implementation
+const ItemContentMobile: React.FC = ({
+ item,
+ itemWithSources,
+}) => {
+ const [api] = useAtom(apiAtom);
+ const isOffline = useOfflineMode();
+ const { getDownloadedItemById } = useDownload();
+ const downloadedItem =
+ isOffline && item?.Id ? getDownloadedItemById(item.Id) : null;
+ const { settings } = useSettings();
+ const { orientation } = useOrientation();
+ const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
+ const [user] = useAtom(userAtom);
- const itemColors = useImageColorsReturn({ item });
+ const itemColors = useImageColorsReturn({ item });
- const [loadingLogo, setLoadingLogo] = useState(true);
- const [headerHeight, setHeaderHeight] = useState(350);
+ const [loadingLogo, setLoadingLogo] = useState(true);
+ const [headerHeight, setHeaderHeight] = useState(350);
- const [selectedOptions, setSelectedOptions] = useState<
- SelectedOptions | undefined
- >(undefined);
+ const [selectedOptions, setSelectedOptions] = useState<
+ SelectedOptions | undefined
+ >(undefined);
- // Use itemWithSources for play settings since it has MediaSources data
- const {
- defaultAudioIndex,
- defaultBitrate,
- defaultMediaSource,
- defaultSubtitleIndex,
- } = useDefaultPlaySettings(itemWithSources ?? item, settings);
+ // Use itemWithSources for play settings since it has MediaSources data
+ const playSettingsOptions = useMemo(
+ () => ({ applyLanguagePreferences: true }),
+ [],
+ );
+ const {
+ defaultAudioIndex,
+ defaultBitrate,
+ defaultMediaSource,
+ defaultSubtitleIndex,
+ } = useDefaultPlaySettings(
+ itemWithSources ?? item,
+ settings,
+ playSettingsOptions,
+ );
- const logoUrl = useMemo(
- () => (item ? getLogoImageUrlById({ api, item }) : null),
- [api, item],
- );
+ const logoUrl = useMemo(
+ () => (item ? getLogoImageUrlById({ api, item }) : null),
+ [api, item],
+ );
- const onLogoLoad = React.useCallback(() => {
- setLoadingLogo(false);
- }, []);
+ const onLogoLoad = React.useCallback(() => {
+ setLoadingLogo(false);
+ }, []);
- const loading = useMemo(() => {
- return Boolean(logoUrl && loadingLogo);
- }, [loadingLogo, logoUrl]);
+ const loading = useMemo(() => {
+ return Boolean(logoUrl && loadingLogo);
+ }, [loadingLogo, logoUrl]);
- // Needs to automatically change the selected to the default values for default indexes.
- useEffect(() => {
- // When offline, use the indices stored in userData (the last-used tracks for this file)
- // rather than the server's defaults, so MediaSourceButton reflects what will actually play.
- const offlineUserData = downloadedItem?.userData;
+ // Needs to automatically change the selected to the default values for default indexes.
+ useEffect(() => {
+ // When offline, use the indices stored in userData (the last-used tracks for this file)
+ // rather than the server's defaults, so MediaSourceButton reflects what will actually play.
+ const offlineUserData = downloadedItem?.userData;
- setSelectedOptions(() => ({
- bitrate: defaultBitrate,
- mediaSource: defaultMediaSource ?? undefined,
- subtitleIndex:
- offlineUserData && !offlineUserData.isTranscoded
- ? offlineUserData.subtitleStreamIndex
- : (defaultSubtitleIndex ?? -1),
- audioIndex:
- offlineUserData && !offlineUserData.isTranscoded
- ? offlineUserData.audioStreamIndex
- : defaultAudioIndex,
- }));
- }, [
- defaultAudioIndex,
- defaultBitrate,
- defaultSubtitleIndex,
- defaultMediaSource,
- downloadedItem?.userData?.audioStreamIndex,
- downloadedItem?.userData?.subtitleStreamIndex,
- ]);
+ setSelectedOptions(() => ({
+ bitrate: defaultBitrate,
+ mediaSource: defaultMediaSource ?? undefined,
+ subtitleIndex:
+ offlineUserData && !offlineUserData.isTranscoded
+ ? offlineUserData.subtitleStreamIndex
+ : (defaultSubtitleIndex ?? -1),
+ audioIndex:
+ offlineUserData && !offlineUserData.isTranscoded
+ ? offlineUserData.audioStreamIndex
+ : defaultAudioIndex,
+ }));
+ }, [
+ defaultAudioIndex,
+ defaultBitrate,
+ defaultSubtitleIndex,
+ defaultMediaSource,
+ downloadedItem?.userData?.audioStreamIndex,
+ downloadedItem?.userData?.subtitleStreamIndex,
+ downloadedItem?.userData?.isTranscoded,
+ ]);
- useEffect(() => {
- if (!Platform.isTV && itemWithSources) {
- navigation.setOptions({
- headerRight: () =>
- item &&
- (Platform.OS === "ios" ? (
-
-
- {item.Type !== "Program" && (
-
- {!Platform.isTV && (
-
+ useEffect(() => {
+ if (!Platform.isTV && itemWithSources) {
+ navigation.setOptions({
+ headerRight: () =>
+ item &&
+ (Platform.OS === "ios" ? (
+
+
+ {item.Type !== "Program" && (
+
+ {!Platform.isTV && (
+
+ )}
+ {user?.Policy?.IsAdministrator &&
+ !settings.hideRemoteSessionButton && (
+
)}
- {user?.Policy?.IsAdministrator &&
- !settings.hideRemoteSessionButton && (
-
- )}
-
-
- {settings.streamyStatsServerUrl &&
- !settings.hideWatchlistsTab && (
-
- )}
-
- )}
-
- ) : (
-
-
- {item.Type !== "Program" && (
-
- {!Platform.isTV && (
-
+
+
+ {settings.streamyStatsServerUrl &&
+ !settings.hideWatchlistsTab && (
+
)}
- {user?.Policy?.IsAdministrator &&
- !settings.hideRemoteSessionButton && (
-
- )}
-
-
-
- {settings.streamyStatsServerUrl &&
- !settings.hideWatchlistsTab && (
-
- )}
-
- )}
-
- )),
- });
- }
- }, [
- item,
- navigation,
- user,
- itemWithSources,
- settings.hideRemoteSessionButton,
- settings.streamyStatsServerUrl,
- settings.hideWatchlistsTab,
- ]);
-
- useEffect(() => {
- if (item) {
- if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)
- setHeaderHeight(230);
- else if (item.Type === "Movie") setHeaderHeight(500);
- else setHeaderHeight(350);
- }
- }, [item, orientation]);
-
- if (!item || !selectedOptions) return null;
-
- return (
-
-
-
+
+ )}
- }
- logo={
- logoUrl ? (
-
- ) : (
-
- )
- }
- >
-
-
-
+ ) : (
+
+
+ {item.Type !== "Program" && (
+
+ {!Platform.isTV && (
+
+ )}
+ {user?.Policy?.IsAdministrator &&
+ !settings.hideRemoteSessionButton && (
+
+ )}
-
-
-
+
+
+ {settings.streamyStatsServerUrl &&
+ !settings.hideWatchlistsTab && (
+
+ )}
+
+ )}
+
+ )),
+ });
+ }
+ }, [
+ item,
+ navigation,
+ user,
+ itemWithSources,
+ settings.hideRemoteSessionButton,
+ settings.streamyStatsServerUrl,
+ settings.hideWatchlistsTab,
+ ]);
+
+ useEffect(() => {
+ if (item) {
+ if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)
+ setHeaderHeight(230);
+ else if (item.Type === "Movie") setHeaderHeight(500);
+ else setHeaderHeight(350);
+ }
+ }, [item, orientation]);
+
+ if (!item || !selectedOptions) return null;
+
+ return (
+
+
+
+
+ }
+ logo={
+ logoUrl ? (
+
+ ) : (
+
+ )
+ }
+ >
+
+
+
+
+
+
+
+ {!isOffline && (
-
+ )}
- {item.Type === "Episode" && (
-
+
+ {item.Type === "Episode" && (
+
+ )}
+
+ {!isOffline &&
+ selectedOptions.mediaSource?.MediaStreams &&
+ selectedOptions.mediaSource.MediaStreams.length > 0 && (
+
)}
- {!isOffline &&
- selectedOptions.mediaSource?.MediaStreams &&
- selectedOptions.mediaSource.MediaStreams.length > 0 && (
-
+
+
+ {item.Type !== "Program" && (
+ <>
+ {item.Type === "Episode" && !isOffline && (
+
)}
-
+
- {item.Type !== "Program" && (
- <>
- {item.Type === "Episode" && !isOffline && (
-
- )}
+ {!isOffline && }
+ >
+ )}
+
+
+
+ );
+};
-
+// Memoize the mobile component
+const MemoizedItemContentMobile = React.memo(ItemContentMobile);
- {!isOffline && }
- >
- )}
-
-
-
- );
- },
-);
+// Exported component that renders TV or mobile version based on platform
+export const ItemContent: React.FC = (props) => {
+ if (Platform.isTV && ItemContentTV) {
+ return ;
+ }
+ return ;
+};
diff --git a/components/ItemContent.tv.tsx b/components/ItemContent.tv.tsx
new file mode 100644
index 000000000..05d0d1ce2
--- /dev/null
+++ b/components/ItemContent.tv.tsx
@@ -0,0 +1,967 @@
+import { Ionicons } from "@expo/vector-icons";
+import type {
+ BaseItemDto,
+ MediaSourceInfo,
+ MediaStream,
+} from "@jellyfin/sdk/lib/generated-client/models";
+import { getTvShowsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { BlurView } from "expo-blur";
+import { File } from "expo-file-system";
+import { Image } from "expo-image";
+import { useAtom } from "jotai";
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { useTranslation } from "react-i18next";
+import { Alert, Dimensions, ScrollView, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
+import { ItemImage } from "@/components/common/ItemImage";
+import { Text } from "@/components/common/Text";
+import { getItemNavigation } from "@/components/common/TouchableItemRouter";
+import { GenreTags } from "@/components/GenreTags";
+import { TVEpisodeList } from "@/components/series/TVEpisodeList";
+import {
+ TVBackdrop,
+ TVButton,
+ TVCastCrewText,
+ TVCastSection,
+ TVFavoriteButton,
+ TVMetadataBadges,
+ TVOptionButton,
+ TVPlayedButton,
+ TVProgressBar,
+ TVRefreshButton,
+ TVSeriesNavigation,
+ TVTechnicalDetails,
+} from "@/components/tv";
+import type { Track } from "@/components/video-player/controls/types";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
+import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
+import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
+import { useTVOptionModal } from "@/hooks/useTVOptionModal";
+import { useTVSubtitleModal } from "@/hooks/useTVSubtitleModal";
+import { useTVThemeMusic } from "@/hooks/useTVThemeMusic";
+import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
+import { useOfflineMode } from "@/providers/OfflineModeProvider";
+import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
+import { useSettings } from "@/utils/atoms/settings";
+import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
+import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
+import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
+import { formatDuration, runtimeTicksToMinutes } from "@/utils/time";
+
+const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
+
+export type SelectedOptions = {
+ bitrate: Bitrate;
+ mediaSource: MediaSourceInfo | undefined;
+ audioIndex: number | undefined;
+ subtitleIndex: number;
+};
+
+interface ItemContentTVProps {
+ item?: BaseItemDto | null;
+ itemWithSources?: BaseItemDto | null;
+ isLoading?: boolean;
+}
+
+// Export as both ItemContentTV (for direct requires) and ItemContent (for platform-resolved imports)
+export const ItemContentTV: React.FC = React.memo(
+ ({ item, itemWithSources }) => {
+ const typography = useScaledTVTypography();
+ const [api] = useAtom(apiAtom);
+ const [user] = useAtom(userAtom);
+ const isOffline = useOfflineMode();
+ const { settings } = useSettings();
+ const insets = useSafeAreaInsets();
+ const router = useRouter();
+ const { showItemActions } = useTVItemActionModal();
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+
+ const _itemColors = useImageColorsReturn({ item });
+
+ // Auto-play theme music (handles fade in/out and cleanup)
+ useTVThemeMusic(item?.Id);
+
+ // State for first episode card ref (used for focus guide)
+ const [_firstEpisodeRef, setFirstEpisodeRef] = useState(null);
+
+ // Fetch season episodes for episodes
+ const { data: seasonEpisodes = [] } = useQuery({
+ queryKey: ["episodes", item?.SeasonId],
+ queryFn: async () => {
+ if (!api || !user?.Id || !item?.SeriesId || !item?.SeasonId) return [];
+ const res = await getTvShowsApi(api).getEpisodes({
+ seriesId: item.SeriesId,
+ userId: user.Id,
+ seasonId: item.SeasonId,
+ enableUserData: true,
+ fields: ["MediaSources", "Overview"],
+ });
+ return res.data.Items || [];
+ },
+ enabled:
+ !!api &&
+ !!user?.Id &&
+ !!item?.SeriesId &&
+ !!item?.SeasonId &&
+ item?.Type === "Episode",
+ });
+
+ const [selectedOptions, setSelectedOptions] = useState<
+ SelectedOptions | undefined
+ >(undefined);
+
+ // Enable language preference application for TV
+ const playSettingsOptions = useMemo(
+ () => ({ applyLanguagePreferences: true }),
+ [],
+ );
+
+ const {
+ defaultAudioIndex,
+ defaultBitrate,
+ defaultMediaSource,
+ defaultSubtitleIndex,
+ } = useDefaultPlaySettings(
+ itemWithSources ?? item,
+ settings,
+ playSettingsOptions,
+ );
+
+ const logoUrl = useMemo(
+ () => (item ? getLogoImageUrlById({ api, item }) : null),
+ [api, item],
+ );
+
+ // Set default play options
+ useEffect(() => {
+ setSelectedOptions(() => ({
+ bitrate: defaultBitrate,
+ mediaSource: defaultMediaSource ?? undefined,
+ subtitleIndex: defaultSubtitleIndex ?? -1,
+ audioIndex: defaultAudioIndex,
+ }));
+ }, [
+ defaultAudioIndex,
+ defaultBitrate,
+ defaultSubtitleIndex,
+ defaultMediaSource,
+ ]);
+
+ const navigateToPlayer = useCallback(
+ (playbackPosition: string) => {
+ if (!item || !selectedOptions) return;
+
+ const queryParams = new URLSearchParams({
+ itemId: item.Id!,
+ audioIndex: selectedOptions.audioIndex?.toString() ?? "",
+ subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
+ mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
+ bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
+ playbackPosition,
+ offline: isOffline ? "true" : "false",
+ });
+
+ router.push(`/player/direct-player?${queryParams.toString()}`);
+ },
+ [item, selectedOptions, isOffline, router],
+ );
+
+ const handlePlay = () => {
+ if (!item || !selectedOptions) return;
+
+ const hasPlaybackProgress =
+ (item.UserData?.PlaybackPositionTicks ?? 0) > 0;
+
+ if (hasPlaybackProgress) {
+ Alert.alert(
+ t("item_card.resume_playback"),
+ t("item_card.resume_playback_description"),
+ [
+ {
+ text: t("common.cancel"),
+ style: "cancel",
+ },
+ {
+ text: t("item_card.play_from_start"),
+ onPress: () => navigateToPlayer("0"),
+ },
+ {
+ text: t("item_card.continue_from", {
+ time: formatDuration(item.UserData?.PlaybackPositionTicks),
+ }),
+ onPress: () =>
+ navigateToPlayer(
+ item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
+ ),
+ isPreferred: true,
+ },
+ ],
+ );
+ } else {
+ navigateToPlayer("0");
+ }
+ };
+
+ // TV Option Modal hook for quality, audio, media source selectors
+ const { showOptions } = useTVOptionModal();
+
+ // TV Subtitle Modal hook
+ const { showSubtitleModal } = useTVSubtitleModal();
+
+ // State for first actor card ref (used for focus guide)
+ const [_firstActorCardRef, setFirstActorCardRef] = useState(
+ null,
+ );
+
+ // Get available audio tracks
+ const audioTracks = useMemo(() => {
+ const streams = selectedOptions?.mediaSource?.MediaStreams?.filter(
+ (s) => s.Type === "Audio",
+ );
+ return streams ?? [];
+ }, [selectedOptions?.mediaSource]);
+
+ // Get available subtitle tracks (raw MediaStream[] for label lookup)
+ const subtitleStreams = useMemo(() => {
+ const streams = selectedOptions?.mediaSource?.MediaStreams?.filter(
+ (s) => s.Type === "Subtitle",
+ );
+ return streams ?? [];
+ }, [selectedOptions?.mediaSource]);
+
+ // Store handleSubtitleChange in a ref for stable callback reference
+ const handleSubtitleChangeRef = useRef<((index: number) => void) | null>(
+ null,
+ );
+
+ // State to trigger refresh of local subtitles list
+ const [localSubtitlesRefreshKey, setLocalSubtitlesRefreshKey] = useState(0);
+
+ // Starting index for local (client-downloaded) subtitles
+ const LOCAL_SUBTITLE_INDEX_START = -100;
+
+ // Convert MediaStream[] to Track[] for the modal (with setTrack callbacks)
+ // Also includes locally downloaded subtitles from OpenSubtitles
+ const subtitleTracksForModal = useMemo((): Track[] => {
+ const tracks: Track[] = subtitleStreams.map((stream) => ({
+ name:
+ stream.DisplayTitle ||
+ `${stream.Language || "Unknown"} (${stream.Codec})`,
+ index: stream.Index ?? -1,
+ setTrack: () => {
+ handleSubtitleChangeRef.current?.(stream.Index ?? -1);
+ },
+ }));
+
+ // Add locally downloaded subtitles (from OpenSubtitles)
+ if (item?.Id) {
+ const localSubs = getSubtitlesForItem(item.Id);
+ let localIdx = 0;
+ for (const localSub of localSubs) {
+ // Verify file still exists (cache may have been cleared)
+ const subtitleFile = new File(localSub.filePath);
+ if (!subtitleFile.exists) {
+ continue;
+ }
+
+ const localIndex = LOCAL_SUBTITLE_INDEX_START - localIdx;
+ tracks.push({
+ name: localSub.name,
+ index: localIndex,
+ isLocal: true,
+ localPath: localSub.filePath,
+ setTrack: () => {
+ // For ItemContent (outside player), just update the selected index
+ // The actual subtitle will be loaded when playback starts
+ handleSubtitleChangeRef.current?.(localIndex);
+ },
+ });
+ localIdx++;
+ }
+ }
+
+ return tracks;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [subtitleStreams, item?.Id, localSubtitlesRefreshKey]);
+
+ // Get available media sources
+ const mediaSources = useMemo(() => {
+ return (itemWithSources ?? item)?.MediaSources ?? [];
+ }, [item, itemWithSources]);
+
+ // Audio options for selector
+ const audioOptions: TVOptionItem[] = useMemo(() => {
+ return audioTracks.map((track) => ({
+ label:
+ track.DisplayTitle ||
+ `${track.Language || "Unknown"} (${track.Codec})`,
+ value: track.Index!,
+ selected: track.Index === selectedOptions?.audioIndex,
+ }));
+ }, [audioTracks, selectedOptions?.audioIndex]);
+
+ // Media source options for selector
+ const mediaSourceOptions: TVOptionItem[] = useMemo(() => {
+ return mediaSources.map((source) => {
+ const videoStream = source.MediaStreams?.find(
+ (s) => s.Type === "Video",
+ );
+ const displayName =
+ videoStream?.DisplayTitle || source.Name || `Source ${source.Id}`;
+ return {
+ label: displayName,
+ value: source,
+ selected: source.Id === selectedOptions?.mediaSource?.Id,
+ };
+ });
+ }, [mediaSources, selectedOptions?.mediaSource?.Id]);
+
+ // Quality/bitrate options for selector
+ const qualityOptions: TVOptionItem[] = useMemo(() => {
+ return BITRATES.map((bitrate) => ({
+ label: bitrate.key,
+ value: bitrate,
+ selected: bitrate.value === selectedOptions?.bitrate?.value,
+ }));
+ }, [selectedOptions?.bitrate?.value]);
+
+ // Handlers for option changes
+ const handleAudioChange = useCallback((audioIndex: number) => {
+ setSelectedOptions((prev) =>
+ prev ? { ...prev, audioIndex } : undefined,
+ );
+ }, []);
+
+ const handleSubtitleChange = useCallback((subtitleIndex: number) => {
+ setSelectedOptions((prev) =>
+ prev ? { ...prev, subtitleIndex } : undefined,
+ );
+ }, []);
+
+ // Keep the ref updated with the latest callback
+ handleSubtitleChangeRef.current = handleSubtitleChange;
+
+ const handleMediaSourceChange = useCallback(
+ (mediaSource: MediaSourceInfo) => {
+ const defaultAudio = mediaSource.MediaStreams?.find(
+ (s) => s.Type === "Audio" && s.IsDefault,
+ );
+ const defaultSubtitle = mediaSource.MediaStreams?.find(
+ (s) => s.Type === "Subtitle" && s.IsDefault,
+ );
+ setSelectedOptions((prev) =>
+ prev
+ ? {
+ ...prev,
+ mediaSource,
+ audioIndex: defaultAudio?.Index ?? prev.audioIndex,
+ subtitleIndex: defaultSubtitle?.Index ?? -1,
+ }
+ : undefined,
+ );
+ },
+ [],
+ );
+
+ const handleQualityChange = useCallback((bitrate: Bitrate) => {
+ setSelectedOptions((prev) => (prev ? { ...prev, bitrate } : undefined));
+ }, []);
+
+ // Handle server-side subtitle download - invalidate queries to refresh tracks
+ const handleServerSubtitleDownloaded = useCallback(() => {
+ if (item?.Id) {
+ queryClient.invalidateQueries({ queryKey: ["item", item.Id] });
+ }
+ }, [item?.Id, queryClient]);
+
+ // Handle local subtitle download - trigger refresh of subtitle tracks
+ const handleLocalSubtitleDownloaded = useCallback((_path: string) => {
+ // Increment the refresh key to trigger re-computation of subtitleTracksForModal
+ setLocalSubtitlesRefreshKey((prev) => prev + 1);
+ }, []);
+
+ // Refresh subtitle tracks by fetching fresh item data from Jellyfin
+ const refreshSubtitleTracks = useCallback(async (): Promise
}
>
-
-
- updateSettings({ mpvSubtitleScale: Math.round(value * 10) / 10 })
+ {!isTv && (
+ <>
+
+
+ updateSettings({ mpvSubtitleMarginY: value })
+ }
+ />
+
+
+
+
+
+ {alignXLabels[settings?.mpvSubtitleAlignX ?? "center"]}
+
+
+
+ }
+ title='Horizontal Alignment'
+ />
+
+
+
+
+
+ {alignYLabels[settings?.mpvSubtitleAlignY ?? "bottom"]}
+
+
+
+ }
+ title='Vertical Alignment'
+ />
+
+ >
+ )}
+
+
+
+ updateSettings({ mpvSubtitleBackgroundEnabled: value })
}
/>
-
- updateSettings({ mpvSubtitleMarginY: value })}
- />
-
-
-
-
-
- {alignXLabels[settings?.mpvSubtitleAlignX ?? "center"]}
-
-
-
- }
- title='Horizontal Alignment'
- />
-
-
-
-
-
- {alignYLabels[settings?.mpvSubtitleAlignY ?? "bottom"]}
-
-
-
- }
- title='Vertical Alignment'
- />
-
+ {settings.mpvSubtitleBackgroundEnabled && (
+
+
+ updateSettings({ mpvSubtitleBackgroundOpacity: value })
+ }
+ />
+
+ )}
);
diff --git a/components/settings/MpvVoSettings.tsx b/components/settings/MpvVoSettings.tsx
new file mode 100644
index 000000000..164829c41
--- /dev/null
+++ b/components/settings/MpvVoSettings.tsx
@@ -0,0 +1,66 @@
+import { Ionicons } from "@expo/vector-icons";
+import type React from "react";
+import { useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { Platform, View } from "react-native";
+import { PlatformDropdown } from "@/components/PlatformDropdown";
+import { type MpvVoDriver, useSettings } from "@/utils/atoms/settings";
+import { Text } from "../common/Text";
+import { ListGroup } from "../list/ListGroup";
+import { ListItem } from "../list/ListItem";
+
+const VO_DRIVER_OPTIONS: { key: string; value: MpvVoDriver }[] = [
+ { key: "home.settings.vo_driver.gpu_next", value: "gpu-next" },
+ { key: "home.settings.vo_driver.gpu", value: "gpu" },
+];
+
+export const MpvVoSettings: React.FC = () => {
+ const { settings, updateSettings } = useSettings();
+ const { t } = useTranslation();
+
+ const voDriverOptions = useMemo(
+ () => [
+ {
+ options: VO_DRIVER_OPTIONS.map((option) => ({
+ type: "radio" as const,
+ label: t(option.key),
+ value: option.value,
+ selected: option.value === (settings?.mpvVoDriver ?? "gpu-next"),
+ onPress: () => updateSettings({ mpvVoDriver: option.value }),
+ })),
+ },
+ ],
+ [settings?.mpvVoDriver, t, updateSettings],
+ );
+
+ const currentVoDriverLabel = useMemo(() => {
+ const option = VO_DRIVER_OPTIONS.find(
+ (o) => o.value === (settings?.mpvVoDriver ?? "gpu-next"),
+ );
+ return option ? t(option.key) : t("home.settings.vo_driver.gpu_next");
+ }, [settings?.mpvVoDriver, t]);
+
+ // Only show on Android
+ if (Platform.OS !== "android") return null;
+
+ if (!settings) return null;
+
+ return (
+
+
+
+
+ {currentVoDriverLabel}
+
+
+
+ }
+ title={t("home.settings.vo_driver.vo_mode")}
+ />
+
+
+ );
+};
diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx
index fcca24988..7abf10fbd 100644
--- a/components/settings/OtherSettings.tsx
+++ b/components/settings/OtherSettings.tsx
@@ -158,14 +158,6 @@ export const OtherSettings: React.FC = () => {
}
/>
-
-
- updateSettings({ showLargeHomeCarousel: value })
- }
- />
-
router.push("/settings/hide-libraries/page")}
title={t("home.settings.other.hide_libraries")}
diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx
index 526cb8629..77f5453e5 100644
--- a/components/settings/SubtitleToggles.tsx
+++ b/components/settings/SubtitleToggles.tsx
@@ -1,9 +1,10 @@
import { Ionicons } from "@expo/vector-icons";
import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client";
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View, type ViewProps } from "react-native";
import { Switch } from "react-native-gesture-handler";
+import { Input } from "@/components/common/Input";
import { Stepper } from "@/components/inputs/Stepper";
import { useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text";
@@ -23,6 +24,11 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
const cultures = media.cultures;
const { t } = useTranslation();
+ // Local state for OpenSubtitles API key (only commit on blur)
+ const [openSubtitlesApiKey, setOpenSubtitlesApiKey] = useState(
+ settings?.openSubtitlesApiKey || "",
+ );
+
const subtitleModes = [
SubtitlePlaybackMode.Default,
SubtitlePlaybackMode.Smart,
@@ -160,17 +166,55 @@ export const SubtitleToggles: React.FC = ({ ...props }) => {
disabled={pluginSettings?.subtitleSize?.locked}
>
- updateSettings({ subtitleSize: Math.round(value * 100) })
+ updateSettings({ mpvSubtitleScale: Math.round(value * 10) / 10 })
}
/>
+
+ {/* OpenSubtitles API Key for client-side subtitle fetching */}
+
+ {t("home.settings.subtitles.opensubtitles_hint") ||
+ "Enter your OpenSubtitles API key to enable client-side subtitle search as a fallback when your Jellyfin server doesn't have a subtitle provider configured."}
+
+ }
+ >
+
+
+ {t("home.settings.subtitles.opensubtitles_api_key") || "API Key"}
+
+ {
+ updateSettings({ openSubtitlesApiKey });
+ }}
+ autoCapitalize='none'
+ autoCorrect={false}
+ secureTextEntry
+ />
+
+ {t("home.settings.subtitles.opensubtitles_get_key") ||
+ "Get your free API key at opensubtitles.com/en/consumers"}
+
+
+
);
};
diff --git a/components/stacks/NestedTabPageStack.tsx b/components/stacks/NestedTabPageStack.tsx
index db0f9b960..3c5fa57f1 100644
--- a/components/stacks/NestedTabPageStack.tsx
+++ b/components/stacks/NestedTabPageStack.tsx
@@ -1,18 +1,13 @@
-import type { ParamListBase, RouteProp } from "@react-navigation/native";
-import type { NativeStackNavigationOptions } from "@react-navigation/native-stack";
+import { Stack } from "expo-router";
+import type { ComponentProps } from "react";
import { Platform } from "react-native";
import { HeaderBackButton } from "../common/HeaderBackButton";
-type ICommonScreenOptions =
- | NativeStackNavigationOptions
- | ((prop: {
- route: RouteProp;
- navigation: any;
- }) => NativeStackNavigationOptions);
+type ICommonScreenOptions = ComponentProps["options"];
export const commonScreenOptions: ICommonScreenOptions = {
title: "",
- headerShown: true,
+ headerShown: !Platform.isTV,
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
headerBlurEffect: "none",
diff --git a/components/tv/TVActorCard.tsx b/components/tv/TVActorCard.tsx
new file mode 100644
index 000000000..31e4be636
--- /dev/null
+++ b/components/tv/TVActorCard.tsx
@@ -0,0 +1,119 @@
+import { Ionicons } from "@expo/vector-icons";
+import { Image } from "expo-image";
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVActorCardProps {
+ person: {
+ Id?: string | null;
+ Name?: string | null;
+ Role?: string | null;
+ };
+ apiBasePath?: string;
+ onPress: () => void;
+ hasTVPreferredFocus?: boolean;
+}
+
+export const TVActorCard = React.forwardRef(
+ ({ person, apiBasePath, onPress, hasTVPreferredFocus }, ref) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation();
+
+ const imageUrl = person.Id
+ ? `${apiBasePath}/Items/${person.Id}/Images/Primary?fillWidth=280&fillHeight=280&quality=90`
+ : null;
+
+ return (
+
+
+
+ {imageUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {person.Name}
+
+
+ {person.Role && (
+
+ {person.Role}
+
+ )}
+
+
+ );
+ },
+);
diff --git a/components/tv/TVBackdrop.tsx b/components/tv/TVBackdrop.tsx
new file mode 100644
index 000000000..315afe41a
--- /dev/null
+++ b/components/tv/TVBackdrop.tsx
@@ -0,0 +1,56 @@
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { LinearGradient } from "expo-linear-gradient";
+import React from "react";
+import { View } from "react-native";
+import { ItemImage } from "@/components/common/ItemImage";
+
+export interface TVBackdropProps {
+ item: BaseItemDto;
+}
+
+export const TVBackdrop: React.FC = React.memo(({ item }) => {
+ return (
+
+
+ {/* Gradient overlays for readability */}
+
+
+
+ );
+});
diff --git a/components/tv/TVButton.tsx b/components/tv/TVButton.tsx
new file mode 100644
index 000000000..606b21c84
--- /dev/null
+++ b/components/tv/TVButton.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+import { Animated, Pressable, View, type ViewStyle } from "react-native";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVButtonProps {
+ onPress: () => void;
+ children: React.ReactNode;
+ variant?: "primary" | "secondary" | "glass";
+ hasTVPreferredFocus?: boolean;
+ disabled?: boolean;
+ style?: ViewStyle;
+ scaleAmount?: number;
+ square?: boolean;
+ refSetter?: (ref: View | null) => void;
+ nextFocusDown?: number;
+ nextFocusUp?: number;
+}
+
+const getButtonStyles = (
+ variant: "primary" | "secondary" | "glass",
+ focused: boolean,
+) => {
+ switch (variant) {
+ case "glass":
+ return {
+ backgroundColor: focused
+ ? "rgba(255, 255, 255, 0.25)"
+ : "rgba(255, 255, 255, 0.1)",
+ shadowColor: "#fff",
+ borderWidth: 1,
+ borderColor: focused
+ ? "rgba(255, 255, 255, 0.4)"
+ : "rgba(255, 255, 255, 0.15)",
+ };
+ case "secondary":
+ return {
+ backgroundColor: focused
+ ? "rgba(255, 255, 255, 0.3)"
+ : "rgba(255, 255, 255, 0.15)",
+ shadowColor: "#fff",
+ borderWidth: 2,
+ borderColor: focused ? "#fff" : "rgba(255, 255, 255, 0.2)",
+ };
+ default:
+ return {
+ backgroundColor: focused ? "#ffffff" : "rgba(255, 255, 255, 0.9)",
+ shadowColor: "#fff",
+ borderWidth: 1,
+ borderColor: "transparent",
+ };
+ }
+};
+
+export const TVButton: React.FC = ({
+ onPress,
+ children,
+ variant = "primary",
+ hasTVPreferredFocus = false,
+ disabled = false,
+ style,
+ scaleAmount = 1.04,
+ square = false,
+ refSetter,
+ nextFocusDown,
+ nextFocusUp,
+}) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount });
+
+ const buttonStyles = getButtonStyles(variant, focused);
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+};
diff --git a/components/tv/TVCancelButton.tsx b/components/tv/TVCancelButton.tsx
new file mode 100644
index 000000000..46316b7a2
--- /dev/null
+++ b/components/tv/TVCancelButton.tsx
@@ -0,0 +1,63 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { Animated, Pressable } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVCancelButtonProps {
+ onPress: () => void;
+ label?: string;
+ disabled?: boolean;
+}
+
+export const TVCancelButton: React.FC = ({
+ onPress,
+ label = "Cancel",
+ disabled = false,
+}) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05, duration: 120 });
+
+ return (
+
+
+
+
+ {label}
+
+
+
+ );
+};
diff --git a/components/tv/TVCastCrewText.tsx b/components/tv/TVCastCrewText.tsx
new file mode 100644
index 000000000..2c07b497b
--- /dev/null
+++ b/components/tv/TVCastCrewText.tsx
@@ -0,0 +1,78 @@
+import type { BaseItemPerson } from "@jellyfin/sdk/lib/generated-client/models";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+
+export interface TVCastCrewTextProps {
+ director?: BaseItemPerson | null;
+ cast?: BaseItemPerson[];
+ /** Hide the cast section (e.g., when visual cast section is shown) */
+ hideCast?: boolean;
+}
+
+export const TVCastCrewText: React.FC = React.memo(
+ ({ director, cast, hideCast = false }) => {
+ const typography = useScaledTVTypography();
+ const { t } = useTranslation();
+
+ if (!director && (!cast || cast.length === 0)) {
+ return null;
+ }
+
+ return (
+
+
+ {t("item_card.cast_and_crew")}
+
+
+ {director && (
+
+
+ {t("item_card.director")}
+
+
+ {director.Name}
+
+
+ )}
+ {!hideCast && cast && cast.length > 0 && (
+
+
+ {t("item_card.cast")}
+
+
+ {cast.map((c) => c.Name).join(", ")}
+
+
+ )}
+
+
+ );
+ },
+);
diff --git a/components/tv/TVCastSection.tsx b/components/tv/TVCastSection.tsx
new file mode 100644
index 000000000..fa84103b3
--- /dev/null
+++ b/components/tv/TVCastSection.tsx
@@ -0,0 +1,85 @@
+import type { BaseItemPerson } from "@jellyfin/sdk/lib/generated-client/models";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { ScrollView, TVFocusGuideView, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVSizes } from "@/constants/TVSizes";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { TVActorCard } from "./TVActorCard";
+
+export interface TVCastSectionProps {
+ cast: BaseItemPerson[];
+ apiBasePath?: string;
+ onActorPress: (personId: string) => void;
+ /** Setter function for the first actor card ref (for focus guide) */
+ firstActorRefSetter?: (ref: View | null) => void;
+ /** Ref to focus guide destination for upward navigation */
+ upwardFocusDestination?: View | null;
+ /** Custom horizontal padding (overrides default 80) */
+ horizontalPadding?: number;
+}
+
+export const TVCastSection: React.FC = React.memo(
+ ({
+ cast,
+ apiBasePath,
+ onActorPress,
+ firstActorRefSetter,
+ upwardFocusDestination,
+ horizontalPadding = 80,
+ }) => {
+ const typography = useScaledTVTypography();
+ const sizes = useScaledTVSizes();
+ const { t } = useTranslation();
+
+ if (cast.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {t("item_card.cast")}
+
+ {/* Focus guide to direct upward navigation from cast back to options */}
+ {upwardFocusDestination && (
+
+ )}
+
+ {cast.map((person, index) => (
+ {
+ if (person.Id) {
+ onActorPress(person.Id);
+ }
+ }}
+ />
+ ))}
+
+
+ );
+ },
+);
diff --git a/components/tv/TVControlButton.tsx b/components/tv/TVControlButton.tsx
new file mode 100644
index 000000000..9870a6eda
--- /dev/null
+++ b/components/tv/TVControlButton.tsx
@@ -0,0 +1,82 @@
+import { Ionicons } from "@expo/vector-icons";
+import type { FC } from "react";
+import {
+ Pressable,
+ Animated as RNAnimated,
+ StyleSheet,
+ type View,
+} from "react-native";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVControlButtonProps {
+ icon: keyof typeof Ionicons.glyphMap;
+ onPress: () => void;
+ onLongPress?: () => void;
+ onPressOut?: () => void;
+ disabled?: boolean;
+ hasTVPreferredFocus?: boolean;
+ size?: number;
+ delayLongPress?: number;
+ /** Callback ref setter for focus guide destination pattern */
+ refSetter?: (ref: View | null) => void;
+}
+
+export const TVControlButton: FC = ({
+ icon,
+ onPress,
+ onLongPress,
+ onPressOut,
+ disabled,
+ hasTVPreferredFocus,
+ size = 32,
+ delayLongPress = 300,
+ refSetter,
+}) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.15, duration: 120 });
+
+ return (
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ width: scaleSize(64),
+ height: scaleSize(64),
+ borderRadius: scaleSize(32),
+ borderWidth: scaleSize(2),
+ justifyContent: "center",
+ alignItems: "center",
+ },
+});
diff --git a/components/tv/TVFavoriteButton.tsx b/components/tv/TVFavoriteButton.tsx
new file mode 100644
index 000000000..330934e04
--- /dev/null
+++ b/components/tv/TVFavoriteButton.tsx
@@ -0,0 +1,32 @@
+import { Ionicons } from "@expo/vector-icons";
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
+import React from "react";
+import { useFavorite } from "@/hooks/useFavorite";
+import { TVButton } from "./TVButton";
+
+export interface TVFavoriteButtonProps {
+ item: BaseItemDto;
+ disabled?: boolean;
+}
+
+export const TVFavoriteButton: React.FC = ({
+ item,
+ disabled,
+}) => {
+ const { isFavorite, toggleFavorite } = useFavorite(item);
+
+ return (
+
+
+
+ );
+};
diff --git a/components/tv/TVFilterButton.tsx b/components/tv/TVFilterButton.tsx
new file mode 100644
index 000000000..2075495a8
--- /dev/null
+++ b/components/tv/TVFilterButton.tsx
@@ -0,0 +1,80 @@
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVFilterButtonProps {
+ label: string;
+ value: string;
+ onPress: () => void;
+ hasTVPreferredFocus?: boolean;
+ disabled?: boolean;
+ hasActiveFilter?: boolean;
+}
+
+export const TVFilterButton: React.FC = ({
+ label,
+ value,
+ onPress,
+ hasTVPreferredFocus = false,
+ disabled = false,
+ hasActiveFilter = false,
+}) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.04, duration: 120 });
+
+ return (
+
+
+
+ {label ? (
+
+ {label}
+
+ ) : null}
+
+ {value}
+
+
+
+
+ );
+};
diff --git a/components/tv/TVFocusablePoster.tsx b/components/tv/TVFocusablePoster.tsx
new file mode 100644
index 000000000..337cbc2ab
--- /dev/null
+++ b/components/tv/TVFocusablePoster.tsx
@@ -0,0 +1,89 @@
+import React, { useRef, useState } from "react";
+import {
+ Animated,
+ Easing,
+ Pressable,
+ View,
+ type ViewStyle,
+} from "react-native";
+
+export interface TVFocusablePosterProps {
+ children: React.ReactNode;
+ onPress: () => void;
+ onLongPress?: () => void;
+ hasTVPreferredFocus?: boolean;
+ glowColor?: "white" | "purple";
+ scaleAmount?: number;
+ style?: ViewStyle;
+ onFocus?: () => void;
+ onBlur?: () => void;
+ disabled?: boolean;
+ /** When true, the item remains focusable even when disabled (for navigation purposes) */
+ focusableWhenDisabled?: boolean;
+ /** Setter function for the ref (for focus guide destinations) */
+ refSetter?: (ref: View | null) => void;
+}
+
+export const TVFocusablePoster: React.FC = ({
+ children,
+ onPress,
+ onLongPress,
+ hasTVPreferredFocus = false,
+ glowColor = "white",
+ scaleAmount = 1.05,
+ style,
+ onFocus: onFocusProp,
+ onBlur: onBlurProp,
+ disabled = false,
+ focusableWhenDisabled = false,
+ refSetter,
+}) => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+
+ const animateTo = (value: number) =>
+ Animated.timing(scale, {
+ toValue: value,
+ duration: 150,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ const shadowColor = glowColor === "white" ? "#ffffff" : "#a855f7";
+
+ return (
+ {
+ setFocused(true);
+ animateTo(scaleAmount);
+ onFocusProp?.();
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ onBlurProp?.();
+ }}
+ hasTVPreferredFocus={hasTVPreferredFocus && !disabled}
+ disabled={disabled}
+ focusable={!disabled || focusableWhenDisabled}
+ >
+
+ {children}
+
+
+ );
+};
diff --git a/components/tv/TVFocusableProgressBar.tsx b/components/tv/TVFocusableProgressBar.tsx
new file mode 100644
index 000000000..b93eb03d5
--- /dev/null
+++ b/components/tv/TVFocusableProgressBar.tsx
@@ -0,0 +1,190 @@
+import React from "react";
+import {
+ Animated,
+ Pressable,
+ StyleSheet,
+ View,
+ type ViewStyle,
+} from "react-native";
+import type { SharedValue } from "react-native-reanimated";
+import ReanimatedModule, { useAnimatedStyle } from "react-native-reanimated";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+const ReanimatedView = ReanimatedModule.View;
+
+export interface TVFocusableProgressBarProps {
+ /** Progress value (SharedValue) in milliseconds */
+ progress: SharedValue;
+ /** Maximum value in milliseconds */
+ max: SharedValue;
+ /** Cache progress value (SharedValue) in milliseconds */
+ cacheProgress?: SharedValue;
+ /** Chapter positions as percentages (0-100) for tick marks */
+ chapterPositions?: number[];
+ /** Callback when the progress bar receives focus */
+ onFocus?: () => void;
+ /** Callback when the progress bar loses focus */
+ onBlur?: () => void;
+ /** Callback ref setter for focus guide destination pattern */
+ refSetter?: (ref: View | null) => void;
+ /** Whether this component is disabled */
+ disabled?: boolean;
+ /** Whether this component should receive initial focus */
+ hasTVPreferredFocus?: boolean;
+ /** Optional style overrides */
+ style?: ViewStyle;
+}
+
+const PROGRESS_BAR_HEIGHT = scaleSize(14);
+
+export const TVFocusableProgressBar: React.FC =
+ React.memo(
+ ({
+ progress,
+ max,
+ cacheProgress,
+ chapterPositions = [],
+ onFocus,
+ onBlur,
+ refSetter,
+ disabled = false,
+ hasTVPreferredFocus = false,
+ style,
+ }) => {
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({
+ scaleAmount: 1.02,
+ duration: 120,
+ onFocus,
+ onBlur,
+ });
+
+ const progressFillStyle = useAnimatedStyle(() => ({
+ width: `${max.value > 0 ? (progress.value / max.value) * 100 : 0}%`,
+ }));
+
+ const cacheProgressStyle = useAnimatedStyle(() => ({
+ width: `${max.value > 0 && cacheProgress ? (cacheProgress.value / max.value) * 100 : 0}%`,
+ }));
+
+ return (
+
+
+
+
+ {cacheProgress && (
+
+ )}
+
+
+ {/* Chapter markers - positioned outside track to extend above */}
+ {chapterPositions.length > 0 && (
+
+ {chapterPositions.map((position, index) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+ },
+ );
+
+const styles = StyleSheet.create({
+ pressableContainer: {
+ // Add padding for focus scale animation to not clip
+ paddingVertical: scaleSize(8),
+ paddingHorizontal: scaleSize(4),
+ },
+ animatedContainer: {
+ height: PROGRESS_BAR_HEIGHT + scaleSize(8),
+ justifyContent: "center",
+ borderRadius: scaleSize(12),
+ paddingHorizontal: scaleSize(4),
+ },
+ animatedContainerFocused: {
+ // Subtle glow effect when focused
+ shadowColor: "#fff",
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.5,
+ shadowRadius: scaleSize(12),
+ },
+ progressTrackWrapper: {
+ position: "relative",
+ height: PROGRESS_BAR_HEIGHT,
+ },
+ progressTrack: {
+ height: PROGRESS_BAR_HEIGHT,
+ backgroundColor: "rgba(255,255,255,0.2)",
+ borderRadius: scaleSize(8),
+ overflow: "hidden",
+ },
+ progressTrackFocused: {
+ // Brighter track when focused
+ backgroundColor: "rgba(255,255,255,0.35)",
+ },
+ cacheProgress: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ height: "100%",
+ backgroundColor: "rgba(255,255,255,0.3)",
+ borderRadius: scaleSize(8),
+ },
+ progressFill: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ height: "100%",
+ backgroundColor: "#fff",
+ borderRadius: scaleSize(8),
+ },
+ chapterMarkersContainer: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ },
+ chapterMarker: {
+ position: "absolute",
+ width: scaleSize(2),
+ height: PROGRESS_BAR_HEIGHT + scaleSize(5),
+ bottom: 0,
+ backgroundColor: "rgba(255, 255, 255, 0.6)",
+ borderRadius: scaleSize(1),
+ transform: [{ translateX: -scaleSize(1) }],
+ },
+});
diff --git a/components/tv/TVHorizontalList.tsx b/components/tv/TVHorizontalList.tsx
new file mode 100644
index 000000000..4bb61168e
--- /dev/null
+++ b/components/tv/TVHorizontalList.tsx
@@ -0,0 +1,222 @@
+import React, { useCallback } from "react";
+import { FlatList, ScrollView, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVSizes } from "@/constants/TVSizes";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+
+interface TVHorizontalListProps {
+ /** Data items to render */
+ data: T[];
+ /** Unique key extractor */
+ keyExtractor: (item: T, index: number) => string;
+ /** Render function for each item */
+ renderItem: (info: { item: T; index: number }) => React.ReactElement | null;
+ /** Optional section title */
+ title?: string;
+ /** Text to show when data array is empty */
+ emptyText?: string;
+ /** Whether to use FlatList (for large/infinite lists) or ScrollView (for small lists) */
+ useFlatList?: boolean;
+ /** Called when end is reached (only for FlatList) */
+ onEndReached?: () => void;
+ /** Ref for the scroll view */
+ scrollViewRef?: React.RefObject | null>;
+ /** Footer component (only for FlatList) */
+ ListFooterComponent?: React.ReactElement | null;
+ /** Whether this is the first section (for initial focus) */
+ isFirstSection?: boolean;
+ /** Loading state */
+ isLoading?: boolean;
+ /** Skeleton item count when loading */
+ skeletonCount?: number;
+ /** Skeleton render function */
+ renderSkeleton?: () => React.ReactElement;
+ /**
+ * Custom horizontal padding (overrides default sizes.padding.scale).
+ * Use this when the list needs to extend beyond its parent's padding.
+ * The list will use negative margin to extend beyond the parent,
+ * then add this padding inside to align content properly.
+ */
+ horizontalPadding?: number;
+}
+
+/**
+ * TVHorizontalList - A unified horizontal list component for TV.
+ *
+ * Provides consistent spacing and layout for horizontal lists:
+ * - Uses `sizes.gaps.item` (24px default) for gap between items
+ * - Uses `sizes.padding.scale` (20px default) for padding to accommodate focus scale
+ * - Supports both ScrollView (small lists) and FlatList (large/infinite lists)
+ */
+export function TVHorizontalList({
+ data,
+ keyExtractor,
+ renderItem,
+ title,
+ emptyText,
+ useFlatList = false,
+ onEndReached,
+ scrollViewRef,
+ ListFooterComponent,
+ isLoading = false,
+ skeletonCount = 5,
+ renderSkeleton,
+ horizontalPadding,
+}: TVHorizontalListProps) {
+ const sizes = useScaledTVSizes();
+ const typography = useScaledTVTypography();
+
+ // Use custom horizontal padding if provided, otherwise use default scale padding
+ const effectiveHorizontalPadding = horizontalPadding ?? sizes.padding.scale;
+ // Apply negative margin when using custom padding to extend beyond parent
+ const marginHorizontal = horizontalPadding ? -horizontalPadding : 0;
+
+ // Wrap renderItem to add consistent gap
+ const renderItemWithGap = useCallback(
+ ({ item, index }: { item: T; index: number }) => {
+ const isLast = index === data.length - 1;
+ return (
+
+ {renderItem({ item, index })}
+
+ );
+ },
+ [data.length, renderItem, sizes.gaps.item],
+ );
+
+ // Empty state
+ if (!isLoading && data.length === 0 && emptyText) {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {emptyText}
+
+
+ );
+ }
+
+ // Loading state
+ if (isLoading && renderSkeleton) {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {Array.from({ length: skeletonCount }).map((_, i) => (
+ {renderSkeleton()}
+ ))}
+
+
+ );
+ }
+
+ const contentContainerStyle = {
+ paddingHorizontal: effectiveHorizontalPadding,
+ paddingVertical: sizes.padding.scale,
+ };
+
+ const listStyle = {
+ overflow: "visible" as const,
+ marginHorizontal,
+ };
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {useFlatList ? (
+ >}
+ horizontal
+ data={data}
+ keyExtractor={keyExtractor}
+ renderItem={renderItemWithGap}
+ showsHorizontalScrollIndicator={false}
+ removeClippedSubviews={false}
+ style={listStyle}
+ contentContainerStyle={contentContainerStyle}
+ onEndReached={onEndReached}
+ onEndReachedThreshold={0.5}
+ initialNumToRender={5}
+ maxToRenderPerBatch={3}
+ windowSize={5}
+ maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
+ ListFooterComponent={ListFooterComponent}
+ />
+ ) : (
+ }
+ horizontal
+ showsHorizontalScrollIndicator={false}
+ style={listStyle}
+ contentContainerStyle={contentContainerStyle}
+ >
+ {data.map((item, index) => (
+
+ {renderItem({ item, index })}
+
+ ))}
+ {ListFooterComponent}
+
+ )}
+
+ );
+}
diff --git a/components/tv/TVItemCardText.tsx b/components/tv/TVItemCardText.tsx
new file mode 100644
index 000000000..c55110b36
--- /dev/null
+++ b/components/tv/TVItemCardText.tsx
@@ -0,0 +1,34 @@
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import React from "react";
+import { View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+
+export interface TVItemCardTextProps {
+ item: BaseItemDto;
+}
+
+export const TVItemCardText: React.FC = ({ item }) => {
+ const typography = useScaledTVTypography();
+
+ return (
+
+
+ {item.Name}
+
+
+ {item.ProductionYear}
+
+
+ );
+};
diff --git a/components/tv/TVLanguageCard.tsx b/components/tv/TVLanguageCard.tsx
new file mode 100644
index 000000000..027a1ada0
--- /dev/null
+++ b/components/tv/TVLanguageCard.tsx
@@ -0,0 +1,101 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { Animated, Pressable, StyleSheet, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVLanguageCardProps {
+ code: string;
+ name: string;
+ selected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+}
+
+export const TVLanguageCard = React.forwardRef(
+ ({ code, name, selected, hasTVPreferredFocus, onPress }, ref) => {
+ const typography = useScaledTVTypography();
+ const styles = createStyles(typography);
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05 });
+
+ return (
+
+
+
+ {name}
+
+
+ {code.toUpperCase()}
+
+ {selected && !focused && (
+
+
+
+ )}
+
+
+ );
+ },
+);
+
+const createStyles = (typography: ReturnType) =>
+ StyleSheet.create({
+ languageCard: {
+ width: scaleSize(120),
+ height: scaleSize(60),
+ borderRadius: scaleSize(12),
+ justifyContent: "center",
+ alignItems: "center",
+ paddingHorizontal: scaleSize(12),
+ },
+ languageCardText: {
+ fontSize: typography.callout,
+ fontWeight: "500",
+ },
+ languageCardCode: {
+ fontSize: typography.callout,
+ marginTop: scaleSize(2),
+ },
+ checkmark: {
+ position: "absolute",
+ top: scaleSize(8),
+ right: scaleSize(8),
+ },
+ });
diff --git a/components/tv/TVMetadataBadges.tsx b/components/tv/TVMetadataBadges.tsx
new file mode 100644
index 000000000..a3c889e7c
--- /dev/null
+++ b/components/tv/TVMetadataBadges.tsx
@@ -0,0 +1,51 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { View } from "react-native";
+import { Badge } from "@/components/Badge";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+
+export interface TVMetadataBadgesProps {
+ year?: number | null;
+ duration?: string | null;
+ officialRating?: string | null;
+ communityRating?: number | null;
+}
+
+export const TVMetadataBadges: React.FC = React.memo(
+ ({ year, duration, officialRating, communityRating }) => {
+ const typography = useScaledTVTypography();
+
+ return (
+
+ {year != null && (
+
+ {year}
+
+ )}
+ {duration && (
+
+ {duration}
+
+ )}
+ {officialRating && }
+ {communityRating != null && (
+ }
+ />
+ )}
+
+ );
+ },
+);
diff --git a/components/tv/TVNextEpisodeCountdown.tsx b/components/tv/TVNextEpisodeCountdown.tsx
new file mode 100644
index 000000000..47193b0e1
--- /dev/null
+++ b/components/tv/TVNextEpisodeCountdown.tsx
@@ -0,0 +1,270 @@
+import type { Api } from "@jellyfin/sdk";
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
+import { BlurView } from "expo-blur";
+import { type FC, useEffect, useMemo, useRef } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Image,
+ Pressable,
+ Animated as RNAnimated,
+ type View as RNView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import Animated, {
+ cancelAnimation,
+ Easing,
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVNextEpisodeCountdownProps {
+ nextItem: BaseItemDto;
+ api: Api | null;
+ show: boolean;
+ isPlaying: boolean;
+ onFinish: () => void;
+ /** Called when user presses the card to skip to next episode */
+ onPlayNext?: () => void;
+ /** Whether controls are visible - affects card position */
+ controlsVisible?: boolean;
+ /** Callback ref setter for focus guide destination pattern */
+ refSetter?: (ref: RNView | null) => void;
+ /** Whether this component should receive initial focus */
+ hasTVPreferredFocus?: boolean;
+ /** Destination used when moving down from this card */
+ playButtonRef?: RNView | null;
+}
+
+// Position constants
+const BOTTOM_WITH_CONTROLS = scaleSize(300);
+const BOTTOM_WITHOUT_CONTROLS = scaleSize(120);
+
+export const TVNextEpisodeCountdown: FC = ({
+ nextItem,
+ api,
+ show,
+ isPlaying,
+ onFinish,
+ onPlayNext,
+ controlsVisible = false,
+ refSetter,
+ hasTVPreferredFocus = true,
+ playButtonRef: downDestination,
+}) => {
+ const typography = useScaledTVTypography();
+ const { t } = useTranslation();
+ const progress = useSharedValue(0);
+ const cancelled = useSharedValue(false);
+ const onFinishRef = useRef(onFinish);
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({
+ scaleAmount: 1.05,
+ duration: 120,
+ });
+
+ onFinishRef.current = onFinish;
+
+ const imageUrl = getPrimaryImageUrl({
+ api,
+ item: nextItem,
+ width: scaleSize(360),
+ quality: 80,
+ });
+
+ // Animated position based on controls visibility
+ const bottomPosition = useSharedValue(
+ controlsVisible ? BOTTOM_WITH_CONTROLS : BOTTOM_WITHOUT_CONTROLS,
+ );
+
+ useEffect(() => {
+ const target = controlsVisible
+ ? BOTTOM_WITH_CONTROLS
+ : BOTTOM_WITHOUT_CONTROLS;
+ bottomPosition.value = withTiming(target, {
+ duration: 300,
+ easing: Easing.out(Easing.quad),
+ });
+ }, [controlsVisible, bottomPosition]);
+
+ const containerAnimatedStyle = useAnimatedStyle(() => ({
+ bottom: bottomPosition.value,
+ }));
+
+ // Progress animation - pause/resume without resetting
+ const prevShowRef = useRef(false);
+
+ useEffect(() => {
+ const justStartedShowing = show && !prevShowRef.current;
+ prevShowRef.current = show;
+
+ if (!show) {
+ cancelAnimation(progress);
+ progress.value = 0;
+ return;
+ }
+
+ if (justStartedShowing) {
+ progress.value = 0;
+ }
+
+ if (!isPlaying) {
+ cancelAnimation(progress);
+ return;
+ }
+
+ cancelled.value = false;
+
+ // Resume from current position
+ const remainingDuration = (1 - progress.value) * 8000;
+ progress.value = withTiming(
+ 1,
+ { duration: remainingDuration, easing: Easing.linear },
+ (finished) => {
+ if (finished && !cancelled.value) {
+ runOnJS(onFinishRef.current)();
+ }
+ },
+ );
+
+ // Cancel animation on unmount to prevent onFinish from firing after exit
+ return () => {
+ cancelled.value = true;
+ cancelAnimation(progress);
+ };
+ }, [show, isPlaying, progress, cancelled]);
+
+ const progressStyle = useAnimatedStyle(() => ({
+ width: `${progress.value * 100}%`,
+ }));
+
+ const styles = useMemo(() => createStyles(typography), [typography]);
+
+ if (!show) return null;
+
+ return (
+
+
+
+
+
+ {imageUrl && (
+
+ )}
+
+
+ {t("player.next_episode")}
+
+
+ {nextItem.SeriesName}
+
+
+
+ S{nextItem.ParentIndexNumber}E{nextItem.IndexNumber} -{" "}
+ {nextItem.Name}
+
+
+
+
+
+
+
+
+
+
+ {downDestination && (
+
+ )}
+
+ );
+};
+
+const createStyles = (typography: ReturnType) =>
+ StyleSheet.create({
+ container: {
+ position: "absolute",
+ right: scaleSize(80),
+ zIndex: 100,
+ },
+ focusedCard: {
+ shadowColor: "#fff",
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.6,
+ shadowRadius: scaleSize(16),
+ },
+ blur: {
+ borderRadius: scaleSize(16),
+ overflow: "hidden",
+ },
+ innerContainer: {
+ flexDirection: "row",
+ alignItems: "stretch",
+ },
+ thumbnail: {
+ width: scaleSize(180),
+ backgroundColor: "rgba(0,0,0,0.3)",
+ },
+ content: {
+ padding: scaleSize(16),
+ justifyContent: "center",
+ width: scaleSize(280),
+ },
+ label: {
+ fontSize: typography.callout,
+ color: "rgba(255,255,255,0.5)",
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ marginBottom: scaleSize(4),
+ },
+ seriesName: {
+ fontSize: typography.callout,
+ color: "rgba(255,255,255,0.7)",
+ marginBottom: scaleSize(2),
+ },
+ episodeInfo: {
+ fontSize: typography.body,
+ color: "#fff",
+ fontWeight: "600",
+ marginBottom: scaleSize(12),
+ },
+ progressContainer: {
+ height: scaleSize(4),
+ backgroundColor: "rgba(255,255,255,0.2)",
+ borderRadius: scaleSize(2),
+ overflow: "hidden",
+ },
+ progressBar: {
+ height: "100%",
+ backgroundColor: "#fff",
+ borderRadius: scaleSize(2),
+ },
+ returnFocusGuide: {
+ height: 1,
+ width: "100%",
+ },
+ });
diff --git a/components/tv/TVOptionButton.tsx b/components/tv/TVOptionButton.tsx
new file mode 100644
index 000000000..1a3ee51d0
--- /dev/null
+++ b/components/tv/TVOptionButton.tsx
@@ -0,0 +1,123 @@
+import { BlurView } from "expo-blur";
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVOptionButtonProps {
+ label: string;
+ value: string;
+ onPress: () => void;
+ hasTVPreferredFocus?: boolean;
+ maxWidth?: number;
+}
+
+export const TVOptionButton = React.forwardRef(
+ ({ label, value, onPress, hasTVPreferredFocus, maxWidth }, ref) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.02, duration: 120 });
+
+ return (
+
+
+ {focused ? (
+
+
+ {label}
+
+
+ {value}
+
+
+ ) : (
+
+
+
+ {label}
+
+
+ {value}
+
+
+
+ )}
+
+
+ );
+ },
+);
diff --git a/components/tv/TVOptionCard.tsx b/components/tv/TVOptionCard.tsx
new file mode 100644
index 000000000..200f2a9f7
--- /dev/null
+++ b/components/tv/TVOptionCard.tsx
@@ -0,0 +1,107 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVOptionCardProps {
+ label: string;
+ sublabel?: string;
+ selected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+ width?: number;
+ height?: number;
+}
+
+export const TVOptionCard = React.forwardRef(
+ (
+ {
+ label,
+ sublabel,
+ selected,
+ hasTVPreferredFocus = false,
+ onPress,
+ width = 160,
+ height = 75,
+ },
+ ref,
+ ) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05 });
+
+ return (
+
+
+
+ {label}
+
+ {sublabel && (
+
+ {sublabel}
+
+ )}
+ {selected && !focused && (
+
+
+
+ )}
+
+
+ );
+ },
+);
diff --git a/components/tv/TVOptionSelector.tsx b/components/tv/TVOptionSelector.tsx
new file mode 100644
index 000000000..ee9ba16cd
--- /dev/null
+++ b/components/tv/TVOptionSelector.tsx
@@ -0,0 +1,205 @@
+import { BlurView } from "expo-blur";
+import { useEffect, useMemo, useRef, useState } from "react";
+import {
+ Animated,
+ Easing,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { TVCancelButton } from "./TVCancelButton";
+import { TVOptionCard } from "./TVOptionCard";
+
+export type TVOptionItem = {
+ label: string;
+ sublabel?: string;
+ value: T;
+ selected: boolean;
+};
+
+export interface TVOptionSelectorProps {
+ visible: boolean;
+ title: string;
+ options: TVOptionItem[];
+ onSelect: (value: T) => void;
+ onClose: () => void;
+ cancelLabel?: string;
+ cardWidth?: number;
+ cardHeight?: number;
+}
+
+export const TVOptionSelector = ({
+ visible,
+ title,
+ options,
+ onSelect,
+ onClose,
+ cancelLabel = "Cancel",
+ cardWidth = 160,
+ cardHeight = 75,
+}: TVOptionSelectorProps) => {
+ const typography = useScaledTVTypography();
+ const [isReady, setIsReady] = useState(false);
+ const firstCardRef = useRef(null);
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(200)).current;
+
+ const initialSelectedIndex = useMemo(() => {
+ const idx = options.findIndex((o) => o.selected);
+ return idx >= 0 ? idx : 0;
+ }, [options]);
+
+ useEffect(() => {
+ if (visible) {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(200);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+ }
+ }, [visible, overlayOpacity, sheetTranslateY]);
+
+ useEffect(() => {
+ if (visible) {
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => clearTimeout(timer);
+ }
+ setIsReady(false);
+ }, [visible]);
+
+ useEffect(() => {
+ if (isReady && firstCardRef.current) {
+ const timer = setTimeout(() => {
+ (firstCardRef.current as any)?.requestTVFocus?.();
+ }, 50);
+ return () => clearTimeout(timer);
+ }
+ }, [isReady]);
+
+ const styles = useMemo(() => createStyles(typography), [typography]);
+
+ if (!visible) return null;
+
+ return (
+
+
+
+
+ {title}
+ {isReady && (
+
+ {options.map((option, index) => (
+ {
+ onSelect(option.value);
+ onClose();
+ }}
+ width={cardWidth}
+ height={cardHeight}
+ />
+ ))}
+
+ )}
+
+ {isReady && (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+const createStyles = (typography: ReturnType) =>
+ StyleSheet.create({
+ overlay: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ justifyContent: "flex-end",
+ zIndex: 1000,
+ },
+ sheetContainer: {
+ width: "100%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: scaleSize(24),
+ borderTopRightRadius: scaleSize(24),
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: scaleSize(24),
+ paddingBottom: scaleSize(50),
+ overflow: "visible",
+ },
+ title: {
+ fontSize: typography.callout,
+ fontWeight: "500",
+ color: "rgba(255,255,255,0.6)",
+ marginBottom: scaleSize(16),
+ paddingHorizontal: scaleSize(48),
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ },
+ scrollView: {
+ overflow: "visible",
+ },
+ scrollContent: {
+ paddingHorizontal: scaleSize(48),
+ paddingVertical: scaleSize(20),
+ gap: scaleSize(12),
+ },
+ cancelButtonContainer: {
+ marginTop: scaleSize(16),
+ paddingHorizontal: scaleSize(48),
+ alignItems: "flex-start",
+ },
+ });
diff --git a/components/tv/TVPlayedButton.tsx b/components/tv/TVPlayedButton.tsx
new file mode 100644
index 000000000..8ab8e4bb5
--- /dev/null
+++ b/components/tv/TVPlayedButton.tsx
@@ -0,0 +1,33 @@
+import { Ionicons } from "@expo/vector-icons";
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
+import React from "react";
+import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
+import { TVButton } from "./TVButton";
+
+export interface TVPlayedButtonProps {
+ item: BaseItemDto;
+ disabled?: boolean;
+}
+
+export const TVPlayedButton: React.FC = ({
+ item,
+ disabled,
+}) => {
+ const isPlayed = item.UserData?.Played ?? false;
+ const toggle = useMarkAsPlayed([item]);
+
+ return (
+ toggle(!isPlayed)}
+ variant='glass'
+ square
+ disabled={disabled}
+ >
+
+
+ );
+};
diff --git a/components/tv/TVPosterCard.tsx b/components/tv/TVPosterCard.tsx
new file mode 100644
index 000000000..fad7261be
--- /dev/null
+++ b/components/tv/TVPosterCard.tsx
@@ -0,0 +1,593 @@
+import { Ionicons } from "@expo/vector-icons";
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { Image } from "expo-image";
+import { useAtomValue } from "jotai";
+import React, { useMemo, useRef, useState } from "react";
+import {
+ Animated,
+ Easing,
+ Pressable,
+ View,
+ type ViewStyle,
+} from "react-native";
+import { ProgressBar } from "@/components/common/ProgressBar";
+import { Text } from "@/components/common/Text";
+import { WatchedIndicator } from "@/components/WatchedIndicator";
+import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import {
+ GlassPosterView,
+ isGlassEffectAvailable,
+} from "@/modules/glass-poster";
+import { apiAtom } from "@/providers/JellyfinProvider";
+import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
+import { scaleSize } from "@/utils/scaleSize";
+import { runtimeTicksToMinutes } from "@/utils/time";
+
+export interface TVPosterCardProps {
+ item: BaseItemDto;
+ /** Poster orientation: vertical = 10:15 (portrait), horizontal = 16:9 (landscape) */
+ orientation?: "vertical" | "horizontal";
+ /** Show text below the poster (title, subtitle) - default: true */
+ showText?: boolean;
+ /** Show progress bar - default: true for items with progress */
+ showProgress?: boolean;
+ /** Show watched indicator - default: true */
+ showWatchedIndicator?: boolean;
+
+ // Focus props
+ hasTVPreferredFocus?: boolean;
+ disabled?: boolean;
+ /** When true, the item remains focusable even when disabled (for navigation purposes) */
+ focusableWhenDisabled?: boolean;
+
+ /** Shows a "Now Playing" badge on the card */
+ isCurrent?: boolean;
+ /** Show a play button overlay */
+ showPlayButton?: boolean;
+
+ // Handlers
+ onPress: () => void;
+ onLongPress?: () => void;
+ onFocus?: () => void;
+ onBlur?: () => void;
+
+ /** Setter function for the ref (for focus guide destinations) */
+ refSetter?: (ref: View | null) => void;
+
+ /** Custom width - overrides default based on orientation */
+ width?: number;
+
+ /** Custom style for the outer container */
+ style?: ViewStyle;
+
+ /** Glow color for focus state */
+ glowColor?: "white" | "purple";
+
+ /** Scale amount for focus animation */
+ scaleAmount?: number;
+
+ /** Custom image URL getter - if not provided, uses smart URL logic */
+ imageUrlGetter?: (item: BaseItemDto) => string | undefined;
+}
+
+/**
+ * TVPosterCard - Unified poster component for TV interface.
+ *
+ * Combines image rendering, focus handling, and text display into a single component.
+ * Supports both portrait (10:15) and landscape (16:9) orientations.
+ *
+ * Features:
+ * - Glass effect on tvOS 26+ with fallback
+ * - Focus handling with scale animation and glow
+ * - Progress bar and watched indicator
+ * - Smart subtitle text based on item type
+ * - "Now Playing" badge for current items
+ */
+export const TVPosterCard: React.FC = ({
+ item,
+ orientation = "vertical",
+ showText = true,
+ showProgress = true,
+ showWatchedIndicator = true,
+ hasTVPreferredFocus = false,
+ disabled = false,
+ focusableWhenDisabled = false,
+ isCurrent = false,
+ showPlayButton = false,
+ onPress,
+ onLongPress,
+ onFocus: onFocusProp,
+ onBlur: onBlurProp,
+ refSetter,
+ width: customWidth,
+ style,
+ glowColor = "white",
+ scaleAmount = 1.05,
+ imageUrlGetter,
+}) => {
+ const api = useAtomValue(apiAtom);
+ const posterSizes = useScaledTVPosterSizes();
+ const typography = useScaledTVTypography();
+
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+
+ // Determine width based on orientation
+ const width = useMemo(() => {
+ if (customWidth) return customWidth;
+ return orientation === "horizontal"
+ ? posterSizes.episode
+ : posterSizes.poster;
+ }, [customWidth, orientation, posterSizes]);
+
+ const aspectRatio = orientation === "horizontal" ? 16 / 9 : 10 / 15;
+
+ // Smart image URL selection
+ const imageUrl = useMemo(() => {
+ // Use custom getter if provided
+ if (imageUrlGetter) {
+ return imageUrlGetter(item) ?? null;
+ }
+
+ if (!api) return null;
+
+ // Horizontal orientation: prefer thumbs/backdrops for landscape images
+ if (orientation === "horizontal") {
+ // Episode: prefer series thumb image for consistent look (like hero section)
+ if (item.Type === "Episode") {
+ // First try parent/series thumb (horizontal series artwork)
+ if (item.ParentBackdropItemId && item.ParentThumbImageTag) {
+ return `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=700&quality=80&tag=${item.ParentThumbImageTag}`;
+ }
+ // Fall back to episode's own primary image
+ if (item.ImageTags?.Primary) {
+ return `${api.basePath}/Items/${item.Id}/Images/Primary?fillHeight=600&quality=80&tag=${item.ImageTags.Primary}`;
+ }
+ // Last resort: try primary without tag
+ return `${api.basePath}/Items/${item.Id}/Images/Primary?fillHeight=700&quality=80`;
+ }
+
+ // Movie/Series/Program: prefer thumb over primary
+ if (item.ImageTags?.Thumb) {
+ return `${api.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=700&quality=80&tag=${item.ImageTags.Thumb}`;
+ }
+ return `${api.basePath}/Items/${item.Id}/Images/Primary?fillHeight=700&quality=80`;
+ }
+
+ // Vertical orientation: use primary image
+ // For episodes, get the series primary image
+ if (
+ item.Type === "Episode" &&
+ item.SeriesId &&
+ item.SeriesPrimaryImageTag
+ ) {
+ return `${api.basePath}/Items/${item.SeriesId}/Images/Primary?fillHeight=${width * 3}&quality=80&tag=${item.SeriesPrimaryImageTag}`;
+ }
+
+ return getPrimaryImageUrl({
+ api,
+ item,
+ width: width * 2, // 2x for quality on large screens
+ });
+ }, [api, item, orientation, width, imageUrlGetter]);
+
+ // Progress calculation
+ const progress = useMemo(() => {
+ if (!showProgress) return 0;
+
+ if (item.Type === "Program") {
+ if (!item.StartDate || !item.EndDate) return 0;
+ const startDate = new Date(item.StartDate);
+ const endDate = new Date(item.EndDate);
+ const now = new Date();
+ const total = endDate.getTime() - startDate.getTime();
+ if (total <= 0) return 0;
+ const elapsed = now.getTime() - startDate.getTime();
+ return (elapsed / total) * 100;
+ }
+ return item.UserData?.PlayedPercentage || 0;
+ }, [item, showProgress]);
+
+ const isWatched = showWatchedIndicator && item.UserData?.Played === true;
+
+ // Blurhash for placeholder
+ const blurhash = useMemo(() => {
+ const key = item.ImageTags?.Primary as string;
+ return item.ImageBlurHashes?.Primary?.[key];
+ }, [item]);
+
+ // Glass effect availability
+ const useGlass = isGlassEffectAvailable();
+
+ // Focus animation
+ const animateTo = (value: number) =>
+ Animated.timing(scale, {
+ toValue: value,
+ duration: 150,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ const shadowColor = glowColor === "white" ? "#ffffff" : "#a855f7";
+
+ // Text rendering helpers
+ const renderSubtitle = () => {
+ if (!showText) return null;
+
+ // Episode: S#:E# • duration
+ if (item.Type === "Episode") {
+ const season = item.ParentIndexNumber;
+ const ep = item.IndexNumber;
+ const episodeLabel =
+ season !== undefined && ep !== undefined ? `S${season}:E${ep}` : null;
+ const duration = item.RunTimeTicks
+ ? runtimeTicksToMinutes(item.RunTimeTicks)
+ : null;
+
+ return (
+
+ {episodeLabel && (
+
+ {episodeLabel}
+
+ )}
+ {duration && (
+ <>
+
+ •
+
+
+ {duration}
+
+ >
+ )}
+
+ );
+ }
+
+ // Program: channel name
+ if (item.Type === "Program" && item.ChannelName) {
+ return (
+
+ {item.ChannelName}
+
+ );
+ }
+
+ // MusicAlbum: artist
+ if (item.Type === "MusicAlbum") {
+ const artist = item.AlbumArtist || item.Artists?.join(", ");
+ if (artist) {
+ return (
+
+ {artist}
+
+ );
+ }
+ }
+
+ // Audio: artist
+ if (item.Type === "Audio") {
+ const artist = item.Artists?.join(", ") || item.AlbumArtist;
+ if (artist) {
+ return (
+
+ {artist}
+
+ );
+ }
+ }
+
+ // Playlist: track count
+ if (item.Type === "Playlist" && item.ChildCount) {
+ return (
+
+ {item.ChildCount} tracks
+
+ );
+ }
+
+ // Default: production year
+ if (item.ProductionYear) {
+ return (
+
+ {item.ProductionYear}
+
+ );
+ }
+
+ return null;
+ };
+
+ // Now Playing badge component
+ const NowPlayingBadge = isCurrent ? (
+
+
+
+ Now Playing
+
+
+ ) : null;
+
+ // Play button overlay component
+ const PlayButtonOverlay = showPlayButton ? (
+
+
+
+ ) : null;
+
+ // Render poster image
+ const renderPosterImage = () => {
+ // Empty placeholder when no URL
+ if (!imageUrl) {
+ return (
+
+ );
+ }
+
+ // Glass effect rendering (tvOS 26+)
+ if (useGlass) {
+ return (
+
+
+ {PlayButtonOverlay}
+ {NowPlayingBadge}
+
+ );
+ }
+
+ // Fallback rendering for older tvOS versions
+ return (
+
+
+ {PlayButtonOverlay}
+ {NowPlayingBadge}
+
+
+
+ );
+ };
+
+ // Render title based on item type
+ const renderTitle = () => {
+ if (!showText) return null;
+
+ // Episode: show episode name as title
+ if (item.Type === "Episode") {
+ return (
+
+ {item.Name}
+
+ );
+ }
+
+ // MusicArtist: centered text
+ if (item.Type === "MusicArtist") {
+ return (
+
+ {item.Name}
+
+ );
+ }
+
+ // Default: show name
+ return (
+
+ {item.Name}
+
+ );
+ };
+
+ return (
+
+ {
+ setFocused(true);
+ // Only animate scale when not using glass effect (glass handles its own focus visual)
+ if (!useGlass) {
+ animateTo(scaleAmount);
+ }
+ onFocusProp?.();
+ }}
+ onBlur={() => {
+ setFocused(false);
+ if (!useGlass) {
+ animateTo(1);
+ }
+ onBlurProp?.();
+ }}
+ hasTVPreferredFocus={hasTVPreferredFocus && !disabled}
+ disabled={disabled && !focusableWhenDisabled}
+ focusable={!disabled || focusableWhenDisabled}
+ >
+
+ {renderPosterImage()}
+
+
+
+ {/* Text below poster */}
+ {showText && (
+
+ {item.Type === "Episode" ? (
+ <>
+ {renderSubtitle()}
+ {renderTitle()}
+ >
+ ) : (
+ <>
+ {renderTitle()}
+ {renderSubtitle()}
+ >
+ )}
+
+ )}
+
+ );
+};
diff --git a/components/tv/TVProgressBar.tsx b/components/tv/TVProgressBar.tsx
new file mode 100644
index 000000000..e8ee60f86
--- /dev/null
+++ b/components/tv/TVProgressBar.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import { View } from "react-native";
+import { scaleSize } from "@/utils/scaleSize";
+
+export interface TVProgressBarProps {
+ /** Progress value between 0 and 1 */
+ progress: number;
+ /** Background color of the track */
+ trackColor?: string;
+ /** Color of the progress fill */
+ fillColor?: string;
+ /** Maximum width of the progress bar */
+ maxWidth?: number;
+ /** Height of the progress bar */
+ height?: number;
+}
+
+export const TVProgressBar: React.FC = React.memo(
+ ({
+ progress,
+ trackColor = "rgba(255,255,255,0.2)",
+ fillColor = "#ffffff",
+ maxWidth = 400,
+ height = 4,
+ }) => {
+ const clampedProgress = Math.max(0, Math.min(1, progress));
+ const scaledMaxWidth = scaleSize(maxWidth);
+ const scaledHeight = scaleSize(height);
+
+ return (
+
+
+
+
+
+ );
+ },
+);
diff --git a/components/tv/TVRefreshButton.tsx b/components/tv/TVRefreshButton.tsx
new file mode 100644
index 000000000..5e44dd943
--- /dev/null
+++ b/components/tv/TVRefreshButton.tsx
@@ -0,0 +1,70 @@
+import { Ionicons } from "@expo/vector-icons";
+import { type QueryClient, useQueryClient } from "@tanstack/react-query";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { Animated, Easing } from "react-native";
+import { TVButton } from "./TVButton";
+
+export interface TVRefreshButtonProps {
+ itemId: string | undefined;
+ queryClient?: QueryClient;
+}
+
+export const TVRefreshButton: React.FC = ({
+ itemId,
+ queryClient: externalQueryClient,
+}) => {
+ const defaultQueryClient = useQueryClient();
+ const queryClient = externalQueryClient ?? defaultQueryClient;
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const spinValue = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ if (isRefreshing) {
+ spinValue.setValue(0);
+ Animated.loop(
+ Animated.timing(spinValue, {
+ toValue: 1,
+ duration: 1000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ }),
+ ).start();
+ } else {
+ spinValue.stopAnimation();
+ spinValue.setValue(0);
+ }
+ }, [isRefreshing, spinValue]);
+
+ const spin = spinValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: ["0deg", "360deg"],
+ });
+
+ const handleRefresh = useCallback(async () => {
+ if (!itemId || isRefreshing) return;
+
+ setIsRefreshing(true);
+ const minSpinTime = new Promise((resolve) => setTimeout(resolve, 1000));
+ try {
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ["item", itemId] }),
+ minSpinTime,
+ ]);
+ } finally {
+ setIsRefreshing(false);
+ }
+ }, [itemId, queryClient, isRefreshing]);
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/components/tv/TVSeriesNavigation.tsx b/components/tv/TVSeriesNavigation.tsx
new file mode 100644
index 000000000..4f828a4e6
--- /dev/null
+++ b/components/tv/TVSeriesNavigation.tsx
@@ -0,0 +1,78 @@
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { ScrollView, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVSizes } from "@/constants/TVSizes";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { TVSeriesSeasonCard } from "./TVSeriesSeasonCard";
+
+export interface TVSeriesNavigationProps {
+ item: BaseItemDto;
+ seriesImageUrl?: string | null;
+ seasonImageUrl?: string | null;
+ onSeriesPress: () => void;
+ onSeasonPress: () => void;
+}
+
+export const TVSeriesNavigation: React.FC = React.memo(
+ ({ item, seriesImageUrl, seasonImageUrl, onSeriesPress, onSeasonPress }) => {
+ const typography = useScaledTVTypography();
+ const sizes = useScaledTVSizes();
+ const { t } = useTranslation();
+
+ // Only show for episodes with a series
+ if (item.Type !== "Episode" || !item.SeriesId) {
+ return null;
+ }
+
+ return (
+
+
+ {t("item_card.from_this_series") || "From this Series"}
+
+
+ {/* Series card */}
+
+
+ {/* Season card */}
+ {(item.SeasonId || item.ParentId) && (
+
+ )}
+
+
+ );
+ },
+);
diff --git a/components/tv/TVSeriesSeasonCard.tsx b/components/tv/TVSeriesSeasonCard.tsx
new file mode 100644
index 000000000..64535cc92
--- /dev/null
+++ b/components/tv/TVSeriesSeasonCard.tsx
@@ -0,0 +1,164 @@
+import { Ionicons } from "@expo/vector-icons";
+import { Image } from "expo-image";
+import React, { useRef, useState } from "react";
+import { Animated, Easing, Platform, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVSizes } from "@/constants/TVSizes";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import {
+ GlassPosterView,
+ isGlassEffectAvailable,
+} from "@/modules/glass-poster";
+import { scaleSize } from "@/utils/scaleSize";
+
+export interface TVSeriesSeasonCardProps {
+ title: string;
+ subtitle?: string;
+ imageUrl: string | null;
+ onPress: () => void;
+ hasTVPreferredFocus?: boolean;
+ /** Setter function for the ref (for focus guide destinations) */
+ refSetter?: (ref: View | null) => void;
+}
+
+export const TVSeriesSeasonCard: React.FC = ({
+ title,
+ subtitle,
+ imageUrl,
+ onPress,
+ hasTVPreferredFocus,
+ refSetter,
+}) => {
+ const typography = useScaledTVTypography();
+ const sizes = useScaledTVSizes();
+ const [focused, setFocused] = useState(false);
+
+ // Check if glass effect is available (tvOS 26+)
+ const useGlass = Platform.OS === "ios" && isGlassEffectAvailable();
+
+ // Scale animation for focus (only used when NOT using glass effect)
+ const scale = useRef(new Animated.Value(1)).current;
+ const animateTo = (value: number) =>
+ Animated.timing(scale, {
+ toValue: value,
+ duration: 150,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ const renderPoster = () => {
+ if (useGlass) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {imageUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+ );
+ };
+
+ return (
+
+ {
+ setFocused(true);
+ // Only animate scale when not using glass effect (glass handles its own focus visual)
+ if (!useGlass) {
+ animateTo(1.05);
+ }
+ }}
+ onBlur={() => {
+ setFocused(false);
+ if (!useGlass) {
+ animateTo(1);
+ }
+ }}
+ hasTVPreferredFocus={hasTVPreferredFocus}
+ >
+
+ {renderPoster()}
+
+
+
+
+
+ {title}
+
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ );
+};
diff --git a/components/tv/TVSkipSegmentCard.tsx b/components/tv/TVSkipSegmentCard.tsx
new file mode 100644
index 000000000..140fa317f
--- /dev/null
+++ b/components/tv/TVSkipSegmentCard.tsx
@@ -0,0 +1,144 @@
+import { Ionicons } from "@expo/vector-icons";
+import type { FC } from "react";
+import { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Pressable,
+ Animated as RNAnimated,
+ StyleSheet,
+ TVFocusGuideView,
+ type View,
+} from "react-native";
+import Animated, {
+ Easing,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated";
+import { Text } from "@/components/common/Text";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVSkipSegmentCardProps {
+ show: boolean;
+ onPress: () => void;
+ type: "intro" | "credits";
+ /** Whether controls are visible - affects card position */
+ controlsVisible?: boolean;
+ /** Callback ref setter for focus guide destination pattern */
+ refSetter?: (ref: View | null) => void;
+ /** Whether this component should receive initial focus */
+ hasTVPreferredFocus?: boolean;
+ /** Destination used when moving down from this card */
+ playButtonRef?: View | null;
+}
+
+// Position constants - same as TVNextEpisodeCountdown (they're mutually exclusive)
+const BOTTOM_WITH_CONTROLS = 300;
+const BOTTOM_WITHOUT_CONTROLS = 120;
+
+export const TVSkipSegmentCard: FC = ({
+ show,
+ onPress,
+ type,
+ controlsVisible = false,
+ refSetter,
+ hasTVPreferredFocus = true,
+ playButtonRef: downDestination,
+}) => {
+ const { t } = useTranslation();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({
+ scaleAmount: 1.1,
+ duration: 120,
+ });
+
+ // Animated position based on controls visibility
+ const bottomPosition = useSharedValue(
+ controlsVisible ? BOTTOM_WITH_CONTROLS : BOTTOM_WITHOUT_CONTROLS,
+ );
+
+ useEffect(() => {
+ const target = controlsVisible
+ ? BOTTOM_WITH_CONTROLS
+ : BOTTOM_WITHOUT_CONTROLS;
+ bottomPosition.value = withTiming(target, {
+ duration: 300,
+ easing: Easing.out(Easing.quad),
+ });
+ }, [controlsVisible, bottomPosition]);
+
+ const containerAnimatedStyle = useAnimatedStyle(() => ({
+ bottom: bottomPosition.value,
+ }));
+
+ const labelText =
+ type === "intro" ? t("player.skip_intro") : t("player.skip_credits");
+
+ if (!show) return null;
+
+ return (
+
+
+
+
+ {labelText}
+
+
+ {downDestination && (
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ position: "absolute",
+ right: scaleSize(80),
+ zIndex: 100,
+ },
+ button: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: scaleSize(10),
+ paddingHorizontal: scaleSize(18),
+ borderRadius: scaleSize(12),
+ borderWidth: scaleSize(2),
+ gap: scaleSize(8),
+ },
+ label: {
+ fontSize: scaleSize(20),
+ color: "#fff",
+ fontWeight: "600",
+ },
+ returnFocusGuide: {
+ height: 1,
+ width: "100%",
+ },
+});
diff --git a/components/tv/TVSubtitleResultCard.tsx b/components/tv/TVSubtitleResultCard.tsx
new file mode 100644
index 000000000..f1f3ccf92
--- /dev/null
+++ b/components/tv/TVSubtitleResultCard.tsx
@@ -0,0 +1,272 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import {
+ ActivityIndicator,
+ Animated,
+ Pressable,
+ StyleSheet,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import type { SubtitleSearchResult } from "@/hooks/useRemoteSubtitles";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVSubtitleResultCardProps {
+ result: SubtitleSearchResult;
+ hasTVPreferredFocus?: boolean;
+ isDownloading?: boolean;
+ onPress: () => void;
+}
+
+export const TVSubtitleResultCard = React.forwardRef<
+ View,
+ TVSubtitleResultCardProps
+>(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => {
+ const typography = useScaledTVTypography();
+ const styles = createStyles(typography);
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.03 });
+
+ return (
+
+
+ {/* Provider/Source badge */}
+
+
+ {result.providerName}
+
+
+
+ {/* Name */}
+
+ {result.name}
+
+
+ {/* Meta info row */}
+
+ {/* Format */}
+
+ {result.format?.toUpperCase()}
+
+
+ {/* Rating if available */}
+ {result.communityRating !== undefined &&
+ result.communityRating > 0 && (
+
+
+
+ {result.communityRating.toFixed(1)}
+
+
+ )}
+
+ {/* Download count if available */}
+ {result.downloadCount !== undefined && result.downloadCount > 0 && (
+
+
+
+ {result.downloadCount.toLocaleString()}
+
+
+ )}
+
+
+ {/* Flags */}
+
+ {result.isHashMatch && (
+
+ Hash Match
+
+ )}
+ {result.hearingImpaired && (
+
+
+
+ )}
+ {result.aiTranslated && (
+
+ AI
+
+ )}
+
+
+ {/* Loading indicator when downloading */}
+ {isDownloading && (
+
+
+
+ )}
+
+
+ );
+});
+
+const createStyles = (typography: ReturnType) =>
+ StyleSheet.create({
+ resultCard: {
+ width: scaleSize(220),
+ minHeight: scaleSize(120),
+ borderRadius: scaleSize(14),
+ padding: scaleSize(14),
+ borderWidth: scaleSize(1),
+ },
+ providerBadge: {
+ alignSelf: "flex-start",
+ paddingHorizontal: scaleSize(8),
+ paddingVertical: scaleSize(3),
+ borderRadius: scaleSize(6),
+ marginBottom: scaleSize(8),
+ },
+ providerText: {
+ fontSize: typography.callout,
+ fontWeight: "600",
+ textTransform: "uppercase",
+ letterSpacing: 0.5,
+ },
+ resultName: {
+ fontSize: typography.callout,
+ fontWeight: "500",
+ marginBottom: scaleSize(8),
+ lineHeight: scaleSize(18),
+ },
+ resultMeta: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(12),
+ marginBottom: scaleSize(8),
+ },
+ resultMetaText: {
+ fontSize: typography.callout,
+ },
+ ratingContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(3),
+ },
+ downloadCountContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: scaleSize(3),
+ },
+ flagsContainer: {
+ flexDirection: "row",
+ gap: scaleSize(6),
+ flexWrap: "wrap",
+ },
+ flag: {
+ paddingHorizontal: scaleSize(6),
+ paddingVertical: scaleSize(2),
+ borderRadius: scaleSize(4),
+ },
+ flagText: {
+ fontSize: typography.callout,
+ fontWeight: "600",
+ color: "#fff",
+ },
+ downloadingOverlay: {
+ ...StyleSheet.absoluteFill,
+ backgroundColor: "rgba(0,0,0,0.5)",
+ borderRadius: scaleSize(14),
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ });
diff --git a/components/tv/TVTabButton.tsx b/components/tv/TVTabButton.tsx
new file mode 100644
index 000000000..be8ea8c2d
--- /dev/null
+++ b/components/tv/TVTabButton.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import { Animated, Pressable } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVTabButtonProps {
+ label: string;
+ active: boolean;
+ onSelect: () => void;
+ hasTVPreferredFocus?: boolean;
+ switchOnFocus?: boolean;
+ disabled?: boolean;
+}
+
+export const TVTabButton: React.FC = ({
+ label,
+ active,
+ onSelect,
+ hasTVPreferredFocus = false,
+ switchOnFocus = false,
+ disabled = false,
+}) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({
+ scaleAmount: 1.05,
+ duration: 120,
+ onFocus: switchOnFocus ? onSelect : undefined,
+ });
+
+ return (
+
+
+
+ {label}
+
+
+
+ );
+};
diff --git a/components/tv/TVTechnicalDetails.tsx b/components/tv/TVTechnicalDetails.tsx
new file mode 100644
index 000000000..0a0fc970a
--- /dev/null
+++ b/components/tv/TVTechnicalDetails.tsx
@@ -0,0 +1,80 @@
+import type { MediaStream } from "@jellyfin/sdk/lib/generated-client/models";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+
+export interface TVTechnicalDetailsProps {
+ mediaStreams: MediaStream[];
+}
+
+export const TVTechnicalDetails: React.FC = React.memo(
+ ({ mediaStreams }) => {
+ const typography = useScaledTVTypography();
+ const { t } = useTranslation();
+
+ const videoStream = mediaStreams.find((s) => s.Type === "Video");
+ const audioStream = mediaStreams.find((s) => s.Type === "Audio");
+
+ if (!videoStream && !audioStream) {
+ return null;
+ }
+
+ return (
+
+
+ {t("item_card.technical_details")}
+
+
+ {videoStream && (
+
+
+ {t("common.video")}
+
+
+ {videoStream.DisplayTitle ||
+ `${videoStream.Codec?.toUpperCase()} ${videoStream.Width}x${videoStream.Height}`}
+
+
+ )}
+ {audioStream && (
+
+
+ {t("common.audio")}
+
+
+ {audioStream.DisplayTitle ||
+ `${audioStream.Codec?.toUpperCase()} ${audioStream.Channels}ch`}
+
+
+ )}
+
+
+ );
+ },
+);
diff --git a/components/tv/TVThemeMusicIndicator.tsx b/components/tv/TVThemeMusicIndicator.tsx
new file mode 100644
index 000000000..88fd1e1a5
--- /dev/null
+++ b/components/tv/TVThemeMusicIndicator.tsx
@@ -0,0 +1,79 @@
+import { Ionicons } from "@expo/vector-icons";
+import React, { useRef, useState } from "react";
+import { Animated, Easing, Pressable, View } from "react-native";
+import { AnimatedEqualizer } from "@/components/music/AnimatedEqualizer";
+import { scaleSize } from "@/utils/scaleSize";
+
+interface TVThemeMusicIndicatorProps {
+ isPlaying: boolean;
+ isMuted: boolean;
+ hasThemeMusic: boolean;
+ onToggleMute: () => void;
+ disabled?: boolean;
+}
+
+export const TVThemeMusicIndicator: React.FC = ({
+ isPlaying,
+ isMuted,
+ hasThemeMusic,
+ onToggleMute,
+ disabled = false,
+}) => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+
+ const animateTo = (v: number) =>
+ Animated.timing(scale, {
+ toValue: v,
+ duration: 150,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ if (!hasThemeMusic || !isPlaying) return null;
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.15);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ }}
+ disabled={disabled}
+ focusable={!disabled}
+ >
+
+ {isMuted ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/components/tv/TVTrackCard.tsx b/components/tv/TVTrackCard.tsx
new file mode 100644
index 000000000..b2b451625
--- /dev/null
+++ b/components/tv/TVTrackCard.tsx
@@ -0,0 +1,106 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { Animated, Pressable, StyleSheet, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVTrackCardProps {
+ label: string;
+ sublabel?: string;
+ selected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+}
+
+export const TVTrackCard = React.forwardRef(
+ ({ label, sublabel, selected, hasTVPreferredFocus, onPress }, ref) => {
+ const typography = useScaledTVTypography();
+ const styles = createStyles(typography);
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05 });
+
+ return (
+
+
+
+ {label}
+
+ {sublabel && (
+
+ {sublabel}
+
+ )}
+ {selected && !focused && (
+
+
+
+ )}
+
+
+ );
+ },
+);
+
+const createStyles = (typography: ReturnType) =>
+ StyleSheet.create({
+ trackCard: {
+ width: scaleSize(180),
+ height: scaleSize(80),
+ borderRadius: scaleSize(14),
+ justifyContent: "center",
+ alignItems: "center",
+ paddingHorizontal: scaleSize(12),
+ },
+ trackCardText: {
+ fontSize: typography.callout,
+ textAlign: "center",
+ },
+ trackCardSublabel: {
+ fontSize: typography.callout,
+ marginTop: scaleSize(2),
+ },
+ checkmark: {
+ position: "absolute",
+ top: scaleSize(8),
+ right: scaleSize(8),
+ },
+ });
diff --git a/components/tv/TVUserCard.tsx b/components/tv/TVUserCard.tsx
new file mode 100644
index 000000000..b687bf757
--- /dev/null
+++ b/components/tv/TVUserCard.tsx
@@ -0,0 +1,183 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import type { AccountSecurityType } from "@/utils/secureCredentials";
+import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+
+export interface TVUserCardProps {
+ username: string;
+ securityType: AccountSecurityType;
+ hasTVPreferredFocus?: boolean;
+ isCurrent?: boolean;
+ onPress: () => void;
+}
+
+export const TVUserCard = React.forwardRef(
+ (
+ {
+ username,
+ securityType,
+ hasTVPreferredFocus = false,
+ isCurrent = false,
+ onPress,
+ },
+ ref,
+ ) => {
+ const { t } = useTranslation();
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: isCurrent ? 1.02 : 1.05 });
+
+ const getSecurityIcon = (): keyof typeof Ionicons.glyphMap => {
+ switch (securityType) {
+ case "pin":
+ return "keypad";
+ case "password":
+ return "lock-closed";
+ default:
+ return "key";
+ }
+ };
+
+ const getSecurityText = (): string => {
+ switch (securityType) {
+ case "pin":
+ return t("save_account.pin_code");
+ case "password":
+ return t("save_account.password");
+ default:
+ return t("save_account.no_protection");
+ }
+ };
+
+ const getBackgroundColor = () => {
+ if (isCurrent) {
+ return focused ? "rgba(255,255,255,0.15)" : "rgba(255,255,255,0.04)";
+ }
+ return focused ? "#fff" : "rgba(255,255,255,0.08)";
+ };
+
+ const getTextColor = () => {
+ if (isCurrent) {
+ return "rgba(255,255,255,0.4)";
+ }
+ return focused ? "#000" : "#fff";
+ };
+
+ const getSecondaryColor = () => {
+ if (isCurrent) {
+ return "rgba(255,255,255,0.25)";
+ }
+ return focused ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.5)";
+ };
+
+ return (
+
+
+ {/* User Avatar */}
+
+
+
+
+ {/* Text column */}
+
+ {/* Username */}
+
+
+ {username}
+
+ {isCurrent && (
+
+ ({t("home.settings.switch_user.current")})
+
+ )}
+
+
+ {/* Security indicator */}
+
+
+
+ {getSecurityText()}
+
+
+
+
+
+ );
+ },
+);
diff --git a/components/tv/hooks/useTVFocusAnimation.ts b/components/tv/hooks/useTVFocusAnimation.ts
new file mode 100644
index 000000000..b3418c8cb
--- /dev/null
+++ b/components/tv/hooks/useTVFocusAnimation.ts
@@ -0,0 +1,64 @@
+import { useCallback, useRef, useState } from "react";
+import { Animated, Easing } from "react-native";
+import { useInactivity } from "@/providers/InactivityProvider";
+
+export interface UseTVFocusAnimationOptions {
+ scaleAmount?: number;
+ duration?: number;
+ onFocus?: () => void;
+ onBlur?: () => void;
+}
+
+export interface UseTVFocusAnimationReturn {
+ focused: boolean;
+ scale: Animated.Value;
+ handleFocus: () => void;
+ handleBlur: () => void;
+ animatedStyle: { transform: { scale: Animated.Value }[] };
+}
+
+export const useTVFocusAnimation = ({
+ scaleAmount = 1.05,
+ duration = 150,
+ onFocus,
+ onBlur,
+}: UseTVFocusAnimationOptions = {}): UseTVFocusAnimationReturn => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+ const { resetInactivityTimer } = useInactivity();
+
+ const animateTo = useCallback(
+ (value: number) => {
+ Animated.timing(scale, {
+ toValue: value,
+ duration,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+ },
+ [scale, duration],
+ );
+
+ const handleFocus = useCallback(() => {
+ setFocused(true);
+ animateTo(scaleAmount);
+ resetInactivityTimer();
+ onFocus?.();
+ }, [animateTo, scaleAmount, resetInactivityTimer, onFocus]);
+
+ const handleBlur = useCallback(() => {
+ setFocused(false);
+ animateTo(1);
+ onBlur?.();
+ }, [animateTo, onBlur]);
+
+ const animatedStyle = { transform: [{ scale }] };
+
+ return {
+ focused,
+ scale,
+ handleFocus,
+ handleBlur,
+ animatedStyle,
+ };
+};
diff --git a/components/tv/index.ts b/components/tv/index.ts
new file mode 100644
index 000000000..a35104eb0
--- /dev/null
+++ b/components/tv/index.ts
@@ -0,0 +1,70 @@
+// Hooks
+export type {
+ UseTVFocusAnimationOptions,
+ UseTVFocusAnimationReturn,
+} from "./hooks/useTVFocusAnimation";
+export { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
+// Settings components (re-export from settings/)
+export * from "./settings";
+// Item content components
+export type { TVActorCardProps } from "./TVActorCard";
+export { TVActorCard } from "./TVActorCard";
+export type { TVBackdropProps } from "./TVBackdrop";
+export { TVBackdrop } from "./TVBackdrop";
+// Core components
+export type { TVButtonProps } from "./TVButton";
+export { TVButton } from "./TVButton";
+export type { TVCancelButtonProps } from "./TVCancelButton";
+export { TVCancelButton } from "./TVCancelButton";
+export type { TVCastCrewTextProps } from "./TVCastCrewText";
+export { TVCastCrewText } from "./TVCastCrewText";
+export type { TVCastSectionProps } from "./TVCastSection";
+export { TVCastSection } from "./TVCastSection";
+// Player control components
+export type { TVControlButtonProps } from "./TVControlButton";
+export { TVControlButton } from "./TVControlButton";
+export type { TVFavoriteButtonProps } from "./TVFavoriteButton";
+export { TVFavoriteButton } from "./TVFavoriteButton";
+export type { TVFilterButtonProps } from "./TVFilterButton";
+export { TVFilterButton } from "./TVFilterButton";
+export type { TVFocusablePosterProps } from "./TVFocusablePoster";
+export { TVFocusablePoster } from "./TVFocusablePoster";
+export type { TVItemCardTextProps } from "./TVItemCardText";
+export { TVItemCardText } from "./TVItemCardText";
+export type { TVLanguageCardProps } from "./TVLanguageCard";
+export { TVLanguageCard } from "./TVLanguageCard";
+export type { TVMetadataBadgesProps } from "./TVMetadataBadges";
+export { TVMetadataBadges } from "./TVMetadataBadges";
+export type { TVNextEpisodeCountdownProps } from "./TVNextEpisodeCountdown";
+export { TVNextEpisodeCountdown } from "./TVNextEpisodeCountdown";
+export type { TVOptionButtonProps } from "./TVOptionButton";
+export { TVOptionButton } from "./TVOptionButton";
+export type { TVOptionCardProps } from "./TVOptionCard";
+export { TVOptionCard } from "./TVOptionCard";
+export type { TVOptionItem, TVOptionSelectorProps } from "./TVOptionSelector";
+export { TVOptionSelector } from "./TVOptionSelector";
+export type { TVPlayedButtonProps } from "./TVPlayedButton";
+export { TVPlayedButton } from "./TVPlayedButton";
+export type { TVProgressBarProps } from "./TVProgressBar";
+export { TVProgressBar } from "./TVProgressBar";
+export type { TVRefreshButtonProps } from "./TVRefreshButton";
+export { TVRefreshButton } from "./TVRefreshButton";
+export type { TVSeriesNavigationProps } from "./TVSeriesNavigation";
+export { TVSeriesNavigation } from "./TVSeriesNavigation";
+export type { TVSeriesSeasonCardProps } from "./TVSeriesSeasonCard";
+export { TVSeriesSeasonCard } from "./TVSeriesSeasonCard";
+export type { TVSkipSegmentCardProps } from "./TVSkipSegmentCard";
+export { TVSkipSegmentCard } from "./TVSkipSegmentCard";
+export type { TVSubtitleResultCardProps } from "./TVSubtitleResultCard";
+export { TVSubtitleResultCard } from "./TVSubtitleResultCard";
+export type { TVTabButtonProps } from "./TVTabButton";
+export { TVTabButton } from "./TVTabButton";
+export type { TVTechnicalDetailsProps } from "./TVTechnicalDetails";
+export { TVTechnicalDetails } from "./TVTechnicalDetails";
+export { TVThemeMusicIndicator } from "./TVThemeMusicIndicator";
+// Subtitle sheet components
+export type { TVTrackCardProps } from "./TVTrackCard";
+export { TVTrackCard } from "./TVTrackCard";
+// User switching
+export type { TVUserCardProps } from "./TVUserCard";
+export { TVUserCard } from "./TVUserCard";
diff --git a/components/tv/settings/TVLogoutButton.tsx b/components/tv/settings/TVLogoutButton.tsx
new file mode 100644
index 000000000..b001e5071
--- /dev/null
+++ b/components/tv/settings/TVLogoutButton.tsx
@@ -0,0 +1,65 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
+
+export interface TVLogoutButtonProps {
+ onPress: () => void;
+ disabled?: boolean;
+}
+
+export const TVLogoutButton: React.FC = ({
+ onPress,
+ disabled,
+}) => {
+ const { t } = useTranslation();
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.05 });
+
+ return (
+
+
+
+
+ {t("home.settings.log_out_button")}
+
+
+
+
+ );
+};
diff --git a/components/tv/settings/TVSectionHeader.tsx b/components/tv/settings/TVSectionHeader.tsx
new file mode 100644
index 000000000..44e240e1a
--- /dev/null
+++ b/components/tv/settings/TVSectionHeader.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+
+export interface TVSectionHeaderProps {
+ title: string;
+}
+
+export const TVSectionHeader: React.FC = ({ title }) => {
+ const typography = useScaledTVTypography();
+
+ return (
+
+ {title}
+
+ );
+};
diff --git a/components/tv/settings/TVSettingsOptionButton.tsx b/components/tv/settings/TVSettingsOptionButton.tsx
new file mode 100644
index 000000000..6248cf2ce
--- /dev/null
+++ b/components/tv/settings/TVSettingsOptionButton.tsx
@@ -0,0 +1,77 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
+
+export interface TVSettingsOptionButtonProps {
+ label: string;
+ value: string;
+ onPress: () => void;
+ isFirst?: boolean;
+ disabled?: boolean;
+}
+
+export const TVSettingsOptionButton: React.FC = ({
+ label,
+ value,
+ onPress,
+ isFirst,
+ disabled,
+}) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.02 });
+
+ return (
+
+
+
+ {label}
+
+
+
+ {value}
+
+
+
+
+
+ );
+};
diff --git a/components/tv/settings/TVSettingsRow.tsx b/components/tv/settings/TVSettingsRow.tsx
new file mode 100644
index 000000000..1b8422d1c
--- /dev/null
+++ b/components/tv/settings/TVSettingsRow.tsx
@@ -0,0 +1,80 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
+
+export interface TVSettingsRowProps {
+ label: string;
+ value: string;
+ onPress?: () => void;
+ isFirst?: boolean;
+ showChevron?: boolean;
+ disabled?: boolean;
+}
+
+export const TVSettingsRow: React.FC = ({
+ label,
+ value,
+ onPress,
+ isFirst,
+ showChevron = true,
+ disabled,
+}) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.02 });
+
+ return (
+
+
+
+ {label}
+
+
+
+ {value}
+
+ {showChevron && (
+
+ )}
+
+
+
+ );
+};
diff --git a/components/tv/settings/TVSettingsStepper.tsx b/components/tv/settings/TVSettingsStepper.tsx
new file mode 100644
index 000000000..d0ba7b2cb
--- /dev/null
+++ b/components/tv/settings/TVSettingsStepper.tsx
@@ -0,0 +1,134 @@
+import { Ionicons } from "@expo/vector-icons";
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
+
+export interface TVSettingsStepperProps {
+ label: string;
+ value: number;
+ onDecrease: () => void;
+ onIncrease: () => void;
+ formatValue?: (value: number) => string;
+ isFirst?: boolean;
+ disabled?: boolean;
+}
+
+export const TVSettingsStepper: React.FC = ({
+ label,
+ value,
+ onDecrease,
+ onIncrease,
+ formatValue,
+ isFirst,
+ disabled,
+}) => {
+ const typography = useScaledTVTypography();
+ const labelAnim = useTVFocusAnimation({ scaleAmount: 1.02 });
+ const minusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
+ const plusAnim = useTVFocusAnimation({ scaleAmount: 1.1 });
+
+ const displayValue = formatValue ? formatValue(value) : String(value);
+
+ return (
+
+
+
+
+ {label}
+
+
+
+
+
+
+
+
+
+
+ {displayValue}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/tv/settings/TVSettingsTextInput.tsx b/components/tv/settings/TVSettingsTextInput.tsx
new file mode 100644
index 000000000..c5d89ecee
--- /dev/null
+++ b/components/tv/settings/TVSettingsTextInput.tsx
@@ -0,0 +1,92 @@
+import React, { useRef } from "react";
+import { Animated, Pressable, TextInput } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
+
+export interface TVSettingsTextInputProps {
+ label: string;
+ value: string;
+ placeholder?: string;
+ onChangeText: (text: string) => void;
+ onBlur?: () => void;
+ secureTextEntry?: boolean;
+ disabled?: boolean;
+}
+
+export const TVSettingsTextInput: React.FC = ({
+ label,
+ value,
+ placeholder,
+ onChangeText,
+ onBlur,
+ secureTextEntry,
+ disabled,
+}) => {
+ const typography = useScaledTVTypography();
+ const inputRef = useRef(null);
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.02 });
+
+ const handleInputBlur = () => {
+ handleBlur();
+ onBlur?.();
+ };
+
+ return (
+ inputRef.current?.focus()}
+ onFocus={handleFocus}
+ onBlur={handleInputBlur}
+ disabled={disabled}
+ focusable={!disabled}
+ >
+
+
+ {label}
+
+
+
+
+ );
+};
diff --git a/components/tv/settings/TVSettingsToggle.tsx b/components/tv/settings/TVSettingsToggle.tsx
new file mode 100644
index 000000000..57f59a489
--- /dev/null
+++ b/components/tv/settings/TVSettingsToggle.tsx
@@ -0,0 +1,79 @@
+import React from "react";
+import { Animated, Pressable, View } from "react-native";
+import { Text } from "@/components/common/Text";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import { scaleSize } from "@/utils/scaleSize";
+import { useTVFocusAnimation } from "../hooks/useTVFocusAnimation";
+
+export interface TVSettingsToggleProps {
+ label: string;
+ value: boolean;
+ onToggle: (value: boolean) => void;
+ isFirst?: boolean;
+ disabled?: boolean;
+}
+
+export const TVSettingsToggle: React.FC = ({
+ label,
+ value,
+ onToggle,
+ isFirst,
+ disabled,
+}) => {
+ const typography = useScaledTVTypography();
+ const { focused, handleFocus, handleBlur, animatedStyle } =
+ useTVFocusAnimation({ scaleAmount: 1.02 });
+
+ return (
+ onToggle(!value)}
+ onFocus={handleFocus}
+ onBlur={handleBlur}
+ hasTVPreferredFocus={isFirst && !disabled}
+ disabled={disabled}
+ focusable={!disabled}
+ >
+
+
+ {label}
+
+
+
+
+
+
+ );
+};
diff --git a/components/tv/settings/index.ts b/components/tv/settings/index.ts
new file mode 100644
index 000000000..43ff2f63b
--- /dev/null
+++ b/components/tv/settings/index.ts
@@ -0,0 +1,14 @@
+export type { TVLogoutButtonProps } from "./TVLogoutButton";
+export { TVLogoutButton } from "./TVLogoutButton";
+export type { TVSectionHeaderProps } from "./TVSectionHeader";
+export { TVSectionHeader } from "./TVSectionHeader";
+export type { TVSettingsOptionButtonProps } from "./TVSettingsOptionButton";
+export { TVSettingsOptionButton } from "./TVSettingsOptionButton";
+export type { TVSettingsRowProps } from "./TVSettingsRow";
+export { TVSettingsRow } from "./TVSettingsRow";
+export type { TVSettingsStepperProps } from "./TVSettingsStepper";
+export { TVSettingsStepper } from "./TVSettingsStepper";
+export type { TVSettingsTextInputProps } from "./TVSettingsTextInput";
+export { TVSettingsTextInput } from "./TVSettingsTextInput";
+export type { TVSettingsToggleProps } from "./TVSettingsToggle";
+export { TVSettingsToggle } from "./TVSettingsToggle";
diff --git a/components/video-player/controls/BottomControls.tsx b/components/video-player/controls/BottomControls.tsx
index 51abf68c7..b0e32dad4 100644
--- a/components/video-player/controls/BottomControls.tsx
+++ b/components/video-player/controls/BottomControls.tsx
@@ -1,18 +1,34 @@
-import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
-import type { FC } from "react";
-import { View } from "react-native";
+import { Ionicons } from "@expo/vector-icons";
+import type {
+ BaseItemDto,
+ ChapterInfo,
+} from "@jellyfin/sdk/lib/generated-client";
+import { type FC, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Pressable, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
import { type SharedValue } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { ChapterList } from "@/components/chapters/ChapterList";
+import { ChapterTicks } from "@/components/chapters/ChapterTicks";
import { Text } from "@/components/common/Text";
import { useSettings } from "@/utils/atoms/settings";
+import { chapterMarkers, chapterNameAt } from "@/utils/chapters";
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
import SkipButton from "./SkipButton";
import { TimeDisplay } from "./TimeDisplay";
import { TrickplayBubble } from "./TrickplayBubble";
+// Chapter tick height in dp — matches the slider track height for a clean,
+// flush look (no top/bottom overflow).
+const TICK_HEIGHT = 10;
+
interface BottomControlsProps {
item: BaseItemDto;
+ /** Item chapters, used for the tick overlay and chapter list. */
+ chapters?: ChapterInfo[] | null;
+ /** Total media duration in milliseconds. */
+ durationMs: number;
showControls: boolean;
isSliding: boolean;
showRemoteBubble: boolean;
@@ -38,6 +54,8 @@ interface BottomControlsProps {
handleSliderChange: (value: number) => void;
handleTouchStart: () => void;
handleTouchEnd: () => void;
+ /** Programmatic seek (chapter list, hotkeys) — bypasses slide gesture state. */
+ seekTo: (value: number) => void;
// Trickplay props
trickPlayUrl: {
@@ -57,10 +75,15 @@ interface BottomControlsProps {
minutes: number;
seconds: number;
};
+
+ // Chapter props
+ chapterPositions?: number[];
}
export const BottomControls: FC = ({
item,
+ chapters,
+ durationMs,
showControls,
isSliding,
showRemoteBubble,
@@ -84,12 +107,39 @@ export const BottomControls: FC = ({
handleSliderChange,
handleTouchStart,
handleTouchEnd,
+ seekTo,
trickPlayUrl,
trickplayInfo,
time,
+ chapterPositions = [],
}) => {
const { settings } = useSettings();
+ const { t } = useTranslation();
const insets = useSafeAreaInsets();
+ const [chapterListVisible, setChapterListVisible] = useState(false);
+
+ // Only expose chapter UI when there are at least two real markers.
+ const chapterMarkerList = useMemo(
+ () => chapterMarkers(chapters, durationMs),
+ [chapters, durationMs],
+ );
+ const hasChapters = chapterMarkerList.length > 1;
+
+ // Current chapter name for the always-visible header label (live playback).
+ const currentChapterName = useMemo(
+ () => (hasChapters ? chapterNameAt(currentTime, chapters) : null),
+ [hasChapters, currentTime, chapters],
+ );
+
+ // Chapter name at the scrubbed position for the trickplay bubble. `time` is
+ // an {h,m,s} object derived from the slider's dragged value — convert back
+ // to ms for the lookup. Only useful while actively scrubbing.
+ const scrubChapterName = useMemo(() => {
+ if (!hasChapters) return null;
+ const scrubMs =
+ (time.hours * 3600 + time.minutes * 60 + time.seconds) * 1000;
+ return chapterNameAt(scrubMs, chapters);
+ }, [hasChapters, time.hours, time.minutes, time.seconds, chapters]);
return (
= ({
onTouchStart={handleControlsInteraction}
>
= ({
{item?.Type === "Audio" && (
{item?.Album}
)}
+ {currentChapterName ? (
+
+ {currentChapterName}
+
+ ) : null}
-
+
+ {hasChapters && (
+ setChapterListVisible(true)}
+ hitSlop={10}
+ className='justify-center mr-4'
+ accessibilityRole='button'
+ accessibilityLabel={t("chapters.open")}
+ >
+
+
+ )}
= ({
height: 10,
justifyContent: "center",
alignItems: "stretch",
+ // Allow chapter ticks taller than the 10px track to bleed out
+ // top/bottom (RN defaults to overflow: "hidden" on Android).
+ overflow: "visible",
}}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
@@ -203,6 +272,7 @@ export const BottomControls: FC = ({
trickPlayUrl={trickPlayUrl}
trickplayInfo={trickplayInfo}
time={time}
+ chapterName={scrubChapterName}
/>
)
}
@@ -212,6 +282,7 @@ export const BottomControls: FC = ({
minimumValue={min}
maximumValue={max}
/>
+
= ({
/>
+ setChapterListVisible(false)}
+ />
);
};
diff --git a/components/video-player/controls/CenterControls.tsx b/components/video-player/controls/CenterControls.tsx
index 76ce4bed3..c668f3d6e 100644
--- a/components/video-player/controls/CenterControls.tsx
+++ b/components/video-player/controls/CenterControls.tsx
@@ -18,6 +18,12 @@ interface CenterControlsProps {
togglePlay: () => void;
handleSkipBackward: () => void;
handleSkipForward: () => void;
+ // Chapter navigation props
+ hasChapters?: boolean;
+ hasPreviousChapter?: boolean;
+ hasNextChapter?: boolean;
+ goToPreviousChapter?: () => void;
+ goToNextChapter?: () => void;
}
export const CenterControls: FC = ({
@@ -29,6 +35,11 @@ export const CenterControls: FC = ({
togglePlay,
handleSkipBackward,
handleSkipForward,
+ hasChapters = false,
+ hasPreviousChapter = false,
+ hasNextChapter = false,
+ goToPreviousChapter,
+ goToNextChapter,
}) => {
const { settings } = useSettings();
const insets = useSafeAreaInsets();
@@ -44,7 +55,7 @@ export const CenterControls: FC = ({
justifyContent: "space-between",
alignItems: "center",
transform: [{ translateY: -22.5 }],
- paddingHorizontal: "28%",
+ paddingHorizontal: hasChapters ? "18%" : "28%",
}}
pointerEvents={showControls ? "box-none" : "none"}
>
@@ -94,6 +105,20 @@ export const CenterControls: FC = ({
)}
+ {!Platform.isTV && hasChapters && (
+
+
+
+ )}
+
{!isBuffering ? (
@@ -108,6 +133,20 @@ export const CenterControls: FC = ({
+ {!Platform.isTV && hasChapters && (
+
+
+
+ )}
+
{!Platform.isTV && (
track height to extend above) */
+ markerHeight?: number;
+ /** Color of the marker lines */
+ markerColor?: string;
+}
+
+/**
+ * Renders vertical tick marks on the progress bar at chapter positions
+ * Should be overlaid on the slider track
+ */
+export const ChapterMarkers: React.FC = React.memo(
+ ({
+ chapterPositions,
+ style,
+ markerHeight = 15,
+ markerColor = "rgba(255, 255, 255, 0.6)",
+ }) => {
+ if (!chapterPositions.length) {
+ return null;
+ }
+
+ return (
+
+ {chapterPositions.map((position, index) => (
+
+ ))}
+
+ );
+ },
+);
+
+const styles = StyleSheet.create({
+ container: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ },
+ marker: {
+ position: "absolute",
+ width: 2,
+ borderRadius: 1,
+ transform: [{ translateX: -1 }], // Center the marker on its position
+ },
+});
diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx
index 96dfad6b3..1e64f16ad 100644
--- a/components/video-player/controls/Controls.tsx
+++ b/components/video-player/controls/Controls.tsx
@@ -33,6 +33,7 @@ import { CONTROLS_CONSTANTS } from "./constants";
import { EpisodeList } from "./EpisodeList";
import { GestureOverlay } from "./GestureOverlay";
import { HeaderControls } from "./HeaderControls";
+import { useChapterNavigation } from "./hooks/useChapterNavigation";
import { useRemoteControl } from "./hooks/useRemoteControl";
import { useVideoNavigation } from "./hooks/useVideoNavigation";
import { useVideoSlider } from "./hooks/useVideoSlider";
@@ -211,6 +212,21 @@ export const Controls: FC = ({
isSeeking,
});
+ // Chapter navigation hook
+ const {
+ hasChapters,
+ hasPreviousChapter,
+ hasNextChapter,
+ goToPreviousChapter,
+ goToNextChapter,
+ chapterPositions,
+ } = useChapterNavigation({
+ chapters: item.Chapters,
+ progress,
+ maxMs,
+ seek,
+ });
+
const toggleControls = useCallback(() => {
if (showControls) {
setShowAudioSlider(false);
@@ -251,6 +267,7 @@ export const Controls: FC = ({
handleTouchEnd,
handleSliderComplete,
handleSliderChange,
+ seekTo,
} = useVideoSlider({
progress,
isSeeking,
@@ -339,10 +356,15 @@ export const Controls: FC = ({
mediaSource: newMediaSource,
audioIndex: defaultAudioIndex,
subtitleIndex: defaultSubtitleIndex,
- } = getDefaultPlaySettings(item, settings, {
- indexes: previousIndexes,
- source: mediaSource ?? undefined,
- });
+ } = getDefaultPlaySettings(
+ item,
+ settings,
+ {
+ indexes: previousIndexes,
+ source: mediaSource ?? undefined,
+ },
+ { applyLanguagePreferences: true },
+ );
const queryParams = new URLSearchParams({
...(offline && { offline: "true" }),
@@ -481,6 +503,7 @@ export const Controls: FC = ({
getTechnicalInfo={getTechnicalInfo}
playMethod={playMethod}
transcodeReasons={transcodeReasons}
+ mediaSource={mediaSource}
/>
)}
= ({
togglePlay={togglePlay}
handleSkipBackward={handleSkipBackward}
handleSkipForward={handleSkipForward}
+ hasChapters={hasChapters}
+ hasPreviousChapter={hasPreviousChapter}
+ hasNextChapter={hasNextChapter}
+ goToPreviousChapter={goToPreviousChapter}
+ goToNextChapter={goToNextChapter}
/>
= ({
>
= ({
handleSliderChange={handleSliderChange}
handleTouchStart={handleTouchStart}
handleTouchEnd={handleTouchEnd}
+ seekTo={seekTo}
trickPlayUrl={trickPlayUrl}
trickplayInfo={trickplayInfo}
time={isSliding || showRemoteBubble ? time : remoteTime}
+ chapterPositions={chapterPositions}
/>
>
diff --git a/components/video-player/controls/Controls.tv.tsx b/components/video-player/controls/Controls.tv.tsx
new file mode 100644
index 000000000..5ddd4bc38
--- /dev/null
+++ b/components/video-player/controls/Controls.tv.tsx
@@ -0,0 +1,1623 @@
+import type {
+ BaseItemDto,
+ MediaSourceInfo,
+} from "@jellyfin/sdk/lib/generated-client";
+import { useLocalSearchParams } from "expo-router";
+import { useAtomValue } from "jotai";
+import {
+ type FC,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Pressable,
+ StyleSheet,
+ TVFocusGuideView,
+ useWindowDimensions,
+ View,
+} from "react-native";
+import Animated, {
+ Easing,
+ type SharedValue,
+ useAnimatedReaction,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { Text } from "@/components/common/Text";
+import {
+ TVControlButton,
+ TVNextEpisodeCountdown,
+ TVSkipSegmentCard,
+} from "@/components/tv";
+import { TVFocusableProgressBar } from "@/components/tv/TVFocusableProgressBar";
+import { useScaledTVTypography } from "@/constants/TVTypography";
+import useRouter from "@/hooks/useAppRouter";
+import { useCreditSkipper } from "@/hooks/useCreditSkipper";
+import { useIntroSkipper } from "@/hooks/useIntroSkipper";
+import { usePlaybackManager } from "@/hooks/usePlaybackManager";
+import { useTrickplay } from "@/hooks/useTrickplay";
+import { useTVOptionModal } from "@/hooks/useTVOptionModal";
+import { useTVSubtitleModal } from "@/hooks/useTVSubtitleModal";
+import type { TechnicalInfo } from "@/modules/mpv-player";
+import type { DownloadedItem } from "@/providers/Downloads/types";
+import { apiAtom } from "@/providers/JellyfinProvider";
+import { useOfflineMode } from "@/providers/OfflineModeProvider";
+import { useSettings } from "@/utils/atoms/settings";
+import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
+import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
+import { formatTimeString, msToTicks, ticksToMs } from "@/utils/time";
+import { CONTROLS_CONSTANTS } from "./constants";
+import { useVideoContext } from "./contexts/VideoContext";
+import { useChapterNavigation } from "./hooks/useChapterNavigation";
+import { useRemoteControl } from "./hooks/useRemoteControl";
+import { useVideoTime } from "./hooks/useVideoTime";
+import { TechnicalInfoOverlay } from "./TechnicalInfoOverlay";
+import { TrickplayBubble } from "./TrickplayBubble";
+import type { Track } from "./types";
+import { useControlsTimeout } from "./useControlsTimeout";
+
+interface Props {
+ item: BaseItemDto;
+ isPlaying: boolean;
+ isSeeking: SharedValue;
+ cacheProgress: SharedValue;
+ progress: SharedValue;
+ isBuffering?: boolean;
+ showControls: boolean;
+ togglePlay: () => void;
+ setShowControls: (shown: boolean) => void;
+ mediaSource?: MediaSourceInfo | null;
+ seek: (ticks: number) => void;
+ play: () => void;
+ pause: () => void;
+ audioIndex?: number;
+ subtitleIndex?: number;
+ onAudioIndexChange?: (index: number) => void;
+ onSubtitleIndexChange?: (index: number) => void;
+ previousItem?: BaseItemDto | null;
+ nextItem?: BaseItemDto | null;
+ goToPreviousItem?: () => void;
+ goToNextItem?: () => void;
+ onRefreshSubtitleTracks?: () => Promise<
+ import("@jellyfin/sdk/lib/generated-client").MediaStream[]
+ >;
+ addSubtitleFile?: (path: string) => void;
+ showTechnicalInfo?: boolean;
+ onToggleTechnicalInfo?: () => void;
+ getTechnicalInfo?: () => Promise;
+ playMethod?: "DirectPlay" | "DirectStream" | "Transcode";
+ transcodeReasons?: string[];
+ downloadedFiles?: DownloadedItem[];
+}
+
+const TV_SEEKBAR_HEIGHT = 14;
+const TV_AUTO_HIDE_TIMEOUT = 5000;
+
+// Trickplay bubble positioning constants
+const TV_TRICKPLAY_SCALE = 2;
+const TV_TRICKPLAY_BUBBLE_BASE_WIDTH = CONTROLS_CONSTANTS.TILE_WIDTH * 1.5;
+const TV_TRICKPLAY_BUBBLE_WIDTH =
+ TV_TRICKPLAY_BUBBLE_BASE_WIDTH * TV_TRICKPLAY_SCALE;
+const TV_TRICKPLAY_INTERNAL_OFFSET = 62 * TV_TRICKPLAY_SCALE;
+const TV_TRICKPLAY_CENTERING_OFFSET = 98 * TV_TRICKPLAY_SCALE;
+const TV_TRICKPLAY_RIGHT_PADDING = 150;
+const TV_TRICKPLAY_FADE_DURATION = 200;
+
+interface TVTrickplayBubbleProps {
+ trickPlayUrl: {
+ x: number;
+ y: number;
+ url: string;
+ } | null;
+ trickplayInfo: {
+ aspectRatio?: number;
+ data: {
+ TileWidth?: number;
+ TileHeight?: number;
+ };
+ } | null;
+ time: {
+ hours: number;
+ minutes: number;
+ seconds: number;
+ };
+ progress: SharedValue;
+ max: SharedValue;
+ progressBarWidth: number;
+ visible: boolean;
+}
+
+const TVTrickplayBubblePositioned: FC = ({
+ trickPlayUrl,
+ trickplayInfo,
+ time,
+ progress,
+ max,
+ progressBarWidth,
+ visible,
+}) => {
+ const opacity = useSharedValue(0);
+
+ useEffect(() => {
+ opacity.value = withTiming(visible ? 1 : 0, {
+ duration: TV_TRICKPLAY_FADE_DURATION,
+ easing: Easing.out(Easing.quad),
+ });
+ }, [visible, opacity]);
+
+ const minX = TV_TRICKPLAY_INTERNAL_OFFSET;
+ const maxX =
+ progressBarWidth -
+ TV_TRICKPLAY_BUBBLE_WIDTH +
+ TV_TRICKPLAY_INTERNAL_OFFSET +
+ TV_TRICKPLAY_RIGHT_PADDING;
+
+ const animatedStyle = useAnimatedStyle(() => {
+ const progressPercent = max.value > 0 ? progress.value / max.value : 0;
+
+ const xPosition = Math.max(
+ minX,
+ Math.min(
+ maxX,
+ progressPercent * progressBarWidth -
+ TV_TRICKPLAY_BUBBLE_WIDTH / 2 +
+ TV_TRICKPLAY_CENTERING_OFFSET,
+ ),
+ );
+
+ return {
+ transform: [{ translateX: xPosition }],
+ opacity: opacity.value,
+ };
+ });
+
+ return (
+
+
+
+ );
+};
+
+export const Controls: FC = ({
+ item,
+ seek,
+ play: _play,
+ pause: _pause,
+ togglePlay,
+ isPlaying,
+ isSeeking,
+ progress,
+ cacheProgress,
+ showControls,
+ setShowControls,
+ mediaSource,
+ audioIndex,
+ subtitleIndex,
+ onAudioIndexChange,
+ onSubtitleIndexChange,
+ previousItem,
+ nextItem: nextItemProp,
+ goToPreviousItem,
+ goToNextItem: goToNextItemProp,
+ onRefreshSubtitleTracks,
+ addSubtitleFile,
+ showTechnicalInfo,
+ onToggleTechnicalInfo,
+ getTechnicalInfo,
+ playMethod,
+ transcodeReasons,
+ downloadedFiles,
+}) => {
+ const typography = useScaledTVTypography();
+ const insets = useSafeAreaInsets();
+ const { width: screenWidth } = useWindowDimensions();
+ const { t } = useTranslation();
+
+ // Calculate progress bar width (matches the padding used in bottomInner)
+ const progressBarWidth = useMemo(() => {
+ const leftPadding = Math.max(insets.left, 48);
+ const rightPadding = Math.max(insets.right, 48);
+ return screenWidth - leftPadding - rightPadding;
+ }, [screenWidth, insets.left, insets.right]);
+ const api = useAtomValue(apiAtom);
+ const { settings } = useSettings();
+ const router = useRouter();
+ const { bitrateValue } = useLocalSearchParams<{
+ bitrateValue: string;
+ }>();
+
+ const { nextItem: internalNextItem } = usePlaybackManager({
+ item,
+ isOffline: false,
+ });
+
+ const nextItem = nextItemProp ?? internalNextItem;
+
+ // TV Option Modal hook for audio selector
+ const { showOptions } = useTVOptionModal();
+
+ // TV Subtitle Modal hook
+ const { showSubtitleModal } = useTVSubtitleModal();
+
+ // Get subtitle tracks from VideoContext (with proper MPV index mapping)
+ const { subtitleTracks: videoContextSubtitleTracks } = useVideoContext();
+
+ // Track which button should have preferred focus when controls show
+ type LastModalType = "audio" | "subtitle" | "techInfo" | null;
+ const [lastOpenedModal, setLastOpenedModal] = useState(null);
+
+ // Track if play button should have focus (when showing controls via up/down D-pad)
+ const [focusPlayButton, setFocusPlayButton] = useState(false);
+
+ // State for progress bar focus and focus guide refs
+ const [isProgressBarFocused, setIsProgressBarFocused] = useState(false);
+ const [playButtonRef, setPlayButtonRef] = useState(null);
+ const [progressBarRef, setProgressBarRef] = useState(null);
+ const [skipSegmentRef, setSkipSegmentRef] = useState(null);
+ const [nextEpisodeRef, setNextEpisodeRef] = useState(null);
+
+ // Minimal seek bar state (shows only progress bar when seeking while controls hidden)
+ const [showMinimalSeekBar, setShowMinimalSeekBar] = useState(false);
+ const minimalSeekBarOpacity = useSharedValue(0);
+ const minimalSeekBarTimeoutRef = useRef | null>(
+ null,
+ );
+
+ // Ref for the invisible focus-stealing overlay (prevents hidden buttons from receiving select events)
+ const focusOverlayRef = useRef(null);
+
+ const audioTracks = useMemo(() => {
+ return mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") ?? [];
+ }, [mediaSource]);
+
+ const _subtitleTracks = useMemo(() => {
+ return (
+ mediaSource?.MediaStreams?.filter((s) => s.Type === "Subtitle") ?? []
+ );
+ }, [mediaSource]);
+
+ const audioOptions: TVOptionItem[] = useMemo(() => {
+ return audioTracks.map((track) => ({
+ label:
+ track.DisplayTitle || `${track.Language || "Unknown"} (${track.Codec})`,
+ value: track.Index!,
+ selected: track.Index === audioIndex,
+ }));
+ }, [audioTracks, audioIndex]);
+
+ const handleAudioChange = useCallback(
+ (index: number) => {
+ onAudioIndexChange?.(index);
+ },
+ [onAudioIndexChange],
+ );
+
+ const _handleSubtitleChange = useCallback(
+ (index: number) => {
+ onSubtitleIndexChange?.(index);
+ },
+ [onSubtitleIndexChange],
+ );
+
+ // Re-fetch subtitle streams from the server (e.g. after a server-side
+ // download) and map them to the modal's Track shape. setTrack drives the
+ // player through the same handler used for manual subtitle selection.
+ const refreshSubtitleTracks = useCallback(async (): Promise
- {!Platform.isTV && (
+ {/* Rotate toggle is Android-only: iOS does not reliably rotate the
+ player back to portrait programmatically. */}
+ {Platform.OS === "android" && (
= ({
}
},
);
+
+ // Cancel animation on unmount to prevent onFinish from firing after exit
+ return () => {
+ cancelAnimation(progress);
+ };
}
}, [show, onFinish]);
diff --git a/components/video-player/controls/TVSubtitleSheet.tsx b/components/video-player/controls/TVSubtitleSheet.tsx
new file mode 100644
index 000000000..6284b5660
--- /dev/null
+++ b/components/video-player/controls/TVSubtitleSheet.tsx
@@ -0,0 +1,560 @@
+import { Ionicons } from "@expo/vector-icons";
+import type {
+ BaseItemDto,
+ MediaStream,
+} from "@jellyfin/sdk/lib/generated-client";
+import { BlurView } from "expo-blur";
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { useTranslation } from "react-i18next";
+import {
+ ActivityIndicator,
+ Animated,
+ Easing,
+ ScrollView,
+ StyleSheet,
+ TVFocusGuideView,
+ View,
+} from "react-native";
+import { Text } from "@/components/common/Text";
+import {
+ TVCancelButton,
+ TVLanguageCard,
+ TVSubtitleResultCard,
+ TVTabButton,
+ TVTrackCard,
+} from "@/components/tv";
+import {
+ type SubtitleSearchResult,
+ useRemoteSubtitles,
+} from "@/hooks/useRemoteSubtitles";
+import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api";
+
+interface TVSubtitleSheetProps {
+ visible: boolean;
+ item: BaseItemDto;
+ mediaSourceId?: string | null;
+ subtitleTracks: MediaStream[];
+ currentSubtitleIndex: number;
+ onSubtitleIndexChange: (index: number) => void;
+ onClose: () => void;
+ onServerSubtitleDownloaded?: () => void;
+ onLocalSubtitleDownloaded?: (path: string) => void;
+}
+
+type TabType = "tracks" | "download";
+
+export const TVSubtitleSheet: React.FC = ({
+ visible,
+ item,
+ mediaSourceId,
+ subtitleTracks,
+ currentSubtitleIndex,
+ onSubtitleIndexChange,
+ onClose,
+ onServerSubtitleDownloaded,
+ onLocalSubtitleDownloaded,
+}) => {
+ const { t } = useTranslation();
+
+ console.log(
+ "[TVSubtitleSheet] visible:",
+ visible,
+ "tracks:",
+ subtitleTracks.length,
+ );
+ const [activeTab, setActiveTab] = useState("tracks");
+ const [selectedLanguage, setSelectedLanguage] = useState("eng");
+ const [downloadingId, setDownloadingId] = useState(null);
+ const [hasSearchedThisSession, setHasSearchedThisSession] = useState(false);
+ const [isReady, setIsReady] = useState(false);
+ const [isTabContentReady, setIsTabContentReady] = useState(false);
+ const firstTrackRef = useRef(null);
+
+ const {
+ hasOpenSubtitlesApiKey,
+ isSearching,
+ searchError,
+ searchResults,
+ search,
+ downloadAsync,
+ reset,
+ } = useRemoteSubtitles({
+ itemId: item.Id ?? "",
+ item,
+ mediaSourceId,
+ });
+
+ const resetRef = useRef(reset);
+ resetRef.current = reset;
+
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
+ const sheetTranslateY = useRef(new Animated.Value(300)).current;
+
+ const initialSelectedTrackIndex = useMemo(() => {
+ if (currentSubtitleIndex === -1) return 0;
+ const trackIdx = subtitleTracks.findIndex(
+ (t) => t.Index === currentSubtitleIndex,
+ );
+ return trackIdx >= 0 ? trackIdx + 1 : 0;
+ }, [subtitleTracks, currentSubtitleIndex]);
+
+ useEffect(() => {
+ if (visible) {
+ overlayOpacity.setValue(0);
+ sheetTranslateY.setValue(300);
+
+ Animated.parallel([
+ Animated.timing(overlayOpacity, {
+ toValue: 1,
+ duration: 250,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }),
+ Animated.timing(sheetTranslateY, {
+ toValue: 0,
+ duration: 300,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }),
+ ]).start();
+ }
+ }, [visible, overlayOpacity, sheetTranslateY]);
+
+ useEffect(() => {
+ if (!visible) {
+ setHasSearchedThisSession(false);
+ setActiveTab("tracks");
+ resetRef.current();
+ setIsReady(false);
+ }
+ }, [visible]);
+
+ useEffect(() => {
+ if (visible) {
+ const timer = setTimeout(() => setIsReady(true), 100);
+ return () => clearTimeout(timer);
+ }
+ setIsReady(false);
+ }, [visible]);
+
+ useEffect(() => {
+ if (visible && activeTab === "download" && !hasSearchedThisSession) {
+ search({ language: selectedLanguage });
+ setHasSearchedThisSession(true);
+ }
+ }, [visible, activeTab, hasSearchedThisSession, search, selectedLanguage]);
+
+ useEffect(() => {
+ if (isReady) {
+ setIsTabContentReady(false);
+ const timer = setTimeout(() => setIsTabContentReady(true), 50);
+ return () => clearTimeout(timer);
+ }
+ setIsTabContentReady(false);
+ }, [activeTab, isReady]);
+
+ const handleLanguageSelect = useCallback(
+ (code: string) => {
+ setSelectedLanguage(code);
+ search({ language: code });
+ },
+ [search],
+ );
+
+ const handleTrackSelect = useCallback(
+ (index: number) => {
+ onSubtitleIndexChange(index);
+ onClose();
+ },
+ [onSubtitleIndexChange, onClose],
+ );
+
+ const handleDownload = useCallback(
+ async (result: SubtitleSearchResult) => {
+ setDownloadingId(result.id);
+
+ try {
+ const downloadResult = await downloadAsync(result);
+
+ if (downloadResult.type === "server") {
+ onServerSubtitleDownloaded?.();
+ } else if (downloadResult.type === "local" && downloadResult.path) {
+ onLocalSubtitleDownloaded?.(downloadResult.path);
+ }
+
+ onClose();
+ } catch (error) {
+ console.error("Failed to download subtitle:", error);
+ } finally {
+ setDownloadingId(null);
+ }
+ },
+ [
+ downloadAsync,
+ onServerSubtitleDownloaded,
+ onLocalSubtitleDownloaded,
+ onClose,
+ ],
+ );
+
+ const displayLanguages = useMemo(
+ () => COMMON_SUBTITLE_LANGUAGES.slice(0, 16),
+ [],
+ );
+
+ const trackOptions = useMemo(() => {
+ const noneOption = {
+ label: t("item_card.subtitles.none"),
+ sublabel: undefined as string | undefined,
+ value: -1,
+ selected: currentSubtitleIndex === -1,
+ };
+ const options = subtitleTracks.map((track) => ({
+ label:
+ track.DisplayTitle || `${track.Language || "Unknown"} (${track.Codec})`,
+ sublabel: track.Codec?.toUpperCase(),
+ value: track.Index!,
+ selected: track.Index === currentSubtitleIndex,
+ }));
+ return [noneOption, ...options];
+ }, [subtitleTracks, currentSubtitleIndex, t]);
+
+ if (!visible) return null;
+
+ return (
+
+
+
+
+ {/* Header with tabs */}
+
+
+ {t("item_card.subtitles.label") || "Subtitles"}
+
+
+ {/* Tab bar */}
+
+ setActiveTab("tracks")}
+ />
+ setActiveTab("download")}
+ />
+
+
+
+ {/* Tracks Tab Content */}
+ {activeTab === "tracks" && isTabContentReady && (
+
+
+ {trackOptions.map((option, index) => (
+ handleTrackSelect(option.value)}
+ />
+ ))}
+
+
+ )}
+
+ {/* Download Tab Content */}
+ {activeTab === "download" && isTabContentReady && (
+ <>
+ {/* Language Selector */}
+
+
+ {t("player.language") || "Language"}
+
+
+ {displayLanguages.map((lang, index) => (
+ handleLanguageSelect(lang.code)}
+ />
+ ))}
+
+
+
+ {/* Results Section */}
+
+
+ {t("player.results") || "Results"}
+ {searchResults && ` (${searchResults.length})`}
+
+
+ {/* Loading state */}
+ {isSearching && (
+
+
+
+ {t("player.searching") || "Searching..."}
+
+
+ )}
+
+ {/* Error state */}
+ {searchError && !isSearching && (
+
+
+
+ {t("player.search_failed") || "Search failed"}
+
+
+ {!hasOpenSubtitlesApiKey
+ ? t("player.no_subtitle_provider") ||
+ "No subtitle provider configured on server"
+ : String(searchError)}
+
+
+ )}
+
+ {/* No results */}
+ {searchResults &&
+ searchResults.length === 0 &&
+ !isSearching &&
+ !searchError && (
+
+
+
+ {t("player.no_subtitles_found") ||
+ "No subtitles found"}
+
+
+ )}
+
+ {/* Results list */}
+ {searchResults &&
+ searchResults.length > 0 &&
+ !isSearching && (
+
+ {searchResults.map((result, index) => (
+ handleDownload(result)}
+ />
+ ))}
+
+ )}
+
+
+ {/* API Key hint if no fallback available */}
+ {!hasOpenSubtitlesApiKey && (
+
+
+
+ {t("player.add_opensubtitles_key_hint") ||
+ "Add OpenSubtitles API key in settings for client-side fallback"}
+
+
+ )}
+ >
+ )}
+
+ {/* Cancel button */}
+ {isReady && (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ overlay: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
+ justifyContent: "flex-end",
+ zIndex: 1000,
+ },
+ sheetContainer: {
+ maxHeight: "70%",
+ },
+ blurContainer: {
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ overflow: "hidden",
+ },
+ content: {
+ paddingTop: 24,
+ paddingBottom: 48,
+ },
+ header: {
+ paddingHorizontal: 48,
+ marginBottom: 20,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: "600",
+ color: "#fff",
+ marginBottom: 16,
+ },
+ tabRow: {
+ flexDirection: "row",
+ gap: 24,
+ },
+ section: {
+ marginBottom: 20,
+ },
+ sectionTitle: {
+ fontSize: 14,
+ fontWeight: "500",
+ color: "rgba(255,255,255,0.5)",
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ marginBottom: 12,
+ paddingHorizontal: 48,
+ },
+ tracksScroll: {
+ overflow: "visible",
+ },
+ tracksScrollContent: {
+ paddingHorizontal: 48,
+ paddingVertical: 8,
+ gap: 12,
+ },
+ languageScroll: {
+ overflow: "visible",
+ },
+ languageScrollContent: {
+ paddingHorizontal: 48,
+ paddingVertical: 8,
+ gap: 10,
+ },
+ resultsScroll: {
+ overflow: "visible",
+ },
+ resultsScrollContent: {
+ paddingHorizontal: 48,
+ paddingVertical: 8,
+ gap: 12,
+ },
+ loadingContainer: {
+ paddingVertical: 40,
+ alignItems: "center",
+ },
+ loadingText: {
+ color: "rgba(255,255,255,0.6)",
+ marginTop: 12,
+ fontSize: 14,
+ },
+ errorContainer: {
+ paddingVertical: 40,
+ paddingHorizontal: 48,
+ alignItems: "center",
+ },
+ errorText: {
+ color: "rgba(255,100,100,0.9)",
+ marginTop: 8,
+ fontSize: 16,
+ fontWeight: "500",
+ },
+ errorHint: {
+ color: "rgba(255,255,255,0.5)",
+ marginTop: 4,
+ fontSize: 13,
+ textAlign: "center",
+ },
+ emptyContainer: {
+ paddingVertical: 40,
+ alignItems: "center",
+ },
+ emptyText: {
+ color: "rgba(255,255,255,0.5)",
+ marginTop: 8,
+ fontSize: 14,
+ },
+ apiKeyHint: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ paddingHorizontal: 48,
+ paddingTop: 8,
+ },
+ apiKeyHintText: {
+ color: "rgba(255,255,255,0.4)",
+ fontSize: 12,
+ },
+ cancelButtonContainer: {
+ paddingHorizontal: 48,
+ paddingTop: 20,
+ alignItems: "flex-start",
+ },
+});
diff --git a/components/video-player/controls/TechnicalInfoOverlay.tsx b/components/video-player/controls/TechnicalInfoOverlay.tsx
index 1498990d1..20ec1fc47 100644
--- a/components/video-player/controls/TechnicalInfoOverlay.tsx
+++ b/components/video-player/controls/TechnicalInfoOverlay.tsx
@@ -1,4 +1,12 @@
-import { type FC, memo, useCallback, useEffect, useState } from "react";
+import type { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
+import {
+ type FC,
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
import { Platform, StyleSheet, Text, View } from "react-native";
import Animated, {
Easing,
@@ -7,6 +15,7 @@ import Animated, {
withTiming,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { useScaledTVTypography } from "@/constants/TVTypography";
import type { TechnicalInfo } from "@/modules/mpv-player";
import { useSettings } from "@/utils/atoms/settings";
import { HEADER_LAYOUT } from "./constants";
@@ -19,6 +28,9 @@ interface TechnicalInfoOverlayProps {
getTechnicalInfo: () => Promise;
playMethod?: PlayMethod;
transcodeReasons?: string[];
+ mediaSource?: MediaSourceInfo | null;
+ currentSubtitleIndex?: number;
+ currentAudioIndex?: number;
}
const formatBitrate = (bitsPerSecond: number): string => {
@@ -47,10 +59,51 @@ const formatCodec = (codec: string): string => {
flac: "FLAC",
opus: "Opus",
mp3: "MP3",
+ // Subtitle codecs
+ srt: "SRT",
+ subrip: "SRT",
+ ass: "ASS",
+ ssa: "SSA",
+ webvtt: "WebVTT",
+ vtt: "WebVTT",
+ pgs: "PGS",
+ hdmv_pgs_subtitle: "PGS",
+ dvd_subtitle: "VobSub",
+ dvdsub: "VobSub",
+ mov_text: "MOV Text",
+ cc_dec: "CC",
+ eia_608: "CC",
};
return codecMap[codec.toLowerCase()] || codec.toUpperCase();
};
+const formatAudioChannels = (channels: number): string => {
+ switch (channels) {
+ case 1:
+ return "Mono";
+ case 2:
+ return "Stereo";
+ case 6:
+ return "5.1";
+ case 8:
+ return "7.1";
+ default:
+ return `${channels}ch`;
+ }
+};
+
+const formatVideoRange = (range?: string | null): string | null => {
+ if (!range || range === "SDR") return null;
+ const rangeMap: Record = {
+ HDR10: "HDR10",
+ HDR10Plus: "HDR10+",
+ HLG: "HLG",
+ "Dolby Vision": "Dolby Vision",
+ DolbyVision: "Dolby Vision",
+ };
+ return rangeMap[range] || range;
+};
+
const formatFps = (fps: number): string => {
// Common frame rates
if (Math.abs(fps - 23.976) < 0.01) return "23.976";
@@ -121,18 +174,55 @@ const formatTranscodeReason = (reason: string): string => {
export const TechnicalInfoOverlay: FC = memo(
({
- showControls,
+ showControls: _showControls,
visible,
getTechnicalInfo,
playMethod,
transcodeReasons,
+ mediaSource,
+ currentSubtitleIndex,
+ currentAudioIndex,
}) => {
+ const typography = useScaledTVTypography();
const { settings } = useSettings();
const insets = useSafeAreaInsets();
const [info, setInfo] = useState(null);
const opacity = useSharedValue(0);
+ // Extract stream info from media source
+ const streamInfo = useMemo(() => {
+ if (!mediaSource?.MediaStreams) return null;
+
+ const videoStream = mediaSource.MediaStreams.find(
+ (s) => s.Type === "Video",
+ );
+ const audioStream = mediaSource.MediaStreams.find(
+ (s) =>
+ s.Type === "Audio" &&
+ (currentAudioIndex !== undefined
+ ? s.Index === currentAudioIndex
+ : s.IsDefault),
+ );
+ const subtitleStream = mediaSource.MediaStreams.find(
+ (s) =>
+ s.Type === "Subtitle" &&
+ currentSubtitleIndex !== undefined &&
+ currentSubtitleIndex >= 0 &&
+ s.Index === currentSubtitleIndex,
+ );
+
+ return {
+ container: mediaSource.Container,
+ videoRange: videoStream?.VideoRangeType,
+ bitDepth: videoStream?.BitDepth,
+ audioChannels: audioStream?.Channels,
+ audioCodecFromSource: audioStream?.Codec,
+ subtitleCodec: subtitleStream?.Codec,
+ subtitleTitle: subtitleStream?.DisplayTitle,
+ };
+ }, [mediaSource, currentAudioIndex, currentSubtitleIndex]);
+
// Animate visibility based on visible prop only (stays visible regardless of controls)
useEffect(() => {
opacity.value = withTiming(visible ? 1 : 0, {
@@ -168,64 +258,85 @@ export const TechnicalInfoOverlay: FC = memo(
opacity: opacity.value,
}));
- // Hide on TV platforms
- if (Platform.isTV) return null;
-
// Don't render if not visible
if (!visible) return null;
+ // TV-specific styles
+ const containerStyle = Platform.isTV
+ ? {
+ top: Math.max(insets.top, 48) + 20,
+ left: Math.max(insets.left, 48) + 20,
+ }
+ : {
+ 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,
+ };
+
+ const textStyle = Platform.isTV
+ ? [
+ styles.infoTextTV,
+ { fontSize: typography.body, lineHeight: typography.body * 1.5 },
+ ]
+ : styles.infoText;
+ const reasonStyle = Platform.isTV
+ ? [styles.reasonTextTV, { fontSize: typography.callout }]
+ : styles.reasonText;
+ const boxStyle = Platform.isTV ? styles.infoBoxTV : styles.infoBox;
+
return (
-
+
{playMethod && (
{getPlayMethodLabel(playMethod)}
)}
{transcodeReasons && transcodeReasons.length > 0 && (
-
+
{transcodeReasons.map(formatTranscodeReason).join(", ")}
)}
{info?.videoWidth && info?.videoHeight && (
-
+
{info.videoWidth}x{info.videoHeight}
+ {streamInfo?.bitDepth ? ` ${streamInfo.bitDepth}bit` : ""}
+ {formatVideoRange(streamInfo?.videoRange)
+ ? ` ${formatVideoRange(streamInfo?.videoRange)}`
+ : ""}
)}
{info?.videoCodec && (
-
+
Video: {formatCodec(info.videoCodec)}
{info.fps ? ` @ ${formatFps(info.fps)} fps` : ""}
)}
{info?.audioCodec && (
-
+
Audio: {formatCodec(info.audioCodec)}
+ {streamInfo?.audioChannels
+ ? ` ${formatAudioChannels(streamInfo.audioChannels)}`
+ : ""}
+
+ )}
+ {streamInfo?.subtitleCodec && (
+
+ Subtitle: {formatCodec(streamInfo.subtitleCodec)}
)}
{(info?.videoBitrate || info?.audioBitrate) && (
-
+
Bitrate:{" "}
{info.videoBitrate
? formatBitrate(info.videoBitrate)
@@ -235,18 +346,22 @@ export const TechnicalInfoOverlay: FC = memo(
)}
{info?.cacheSeconds !== undefined && (
-
+
Buffer: {info.cacheSeconds.toFixed(1)}s
)}
+ {info?.voDriver && (
+
+ VO: {info.voDriver}
+ {info.hwdec ? ` / ${info.hwdec}` : ""}
+
+ )}
{info?.droppedFrames !== undefined && info.droppedFrames > 0 && (
-
+
Dropped: {info.droppedFrames} frames
)}
- {!info && !playMethod && (
- Loading...
- )}
+ {!info && !playMethod && Loading...}
);
@@ -267,12 +382,23 @@ const styles = StyleSheet.create({
paddingVertical: 8,
minWidth: 150,
},
+ infoBoxTV: {
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
+ borderRadius: 12,
+ paddingHorizontal: 20,
+ paddingVertical: 16,
+ minWidth: 250,
+ },
infoText: {
color: "white",
fontSize: 12,
fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
lineHeight: 18,
},
+ infoTextTV: {
+ color: "white",
+ fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
+ },
warningText: {
color: "#ff9800",
},
@@ -280,4 +406,7 @@ const styles = StyleSheet.create({
color: "#fbbf24",
fontSize: 10,
},
+ reasonTextTV: {
+ color: "#fbbf24",
+ },
});
diff --git a/components/video-player/controls/TrickplayBubble.tsx b/components/video-player/controls/TrickplayBubble.tsx
index 49645ed2e..e00a19e6a 100644
--- a/components/video-player/controls/TrickplayBubble.tsx
+++ b/components/video-player/controls/TrickplayBubble.tsx
@@ -4,6 +4,12 @@ import { View } from "react-native";
import { Text } from "@/components/common/Text";
import { CONTROLS_CONSTANTS } from "./constants";
+// Slightly larger preview (scale 1.6 vs old 1.4) to give the overlay text
+// more room and feel closer to the Jellyfin web style.
+const BASE_IMAGE_SCALE = 1.6;
+const BUBBLE_LEFT_OFFSET = 62;
+const BUBBLE_WIDTH_MULTIPLIER = 1.5;
+
interface TrickplayBubbleProps {
trickPlayUrl: {
x: number;
@@ -22,12 +28,18 @@ interface TrickplayBubbleProps {
minutes: number;
seconds: number;
};
+ /** Scale factor for the image (default 1). Does not affect timestamp text. */
+ imageScale?: number;
+ /** Chapter name at the scrubbed position, if any. */
+ chapterName?: string | null;
}
export const TrickplayBubble: FC = ({
trickPlayUrl,
trickplayInfo,
time,
+ imageScale = 1,
+ chapterName,
}) => {
if (!trickPlayUrl || !trickplayInfo) {
return null;
@@ -36,18 +48,28 @@ export const TrickplayBubble: FC = ({
const { x, y, url } = trickPlayUrl;
const tileWidth = CONTROLS_CONSTANTS.TILE_WIDTH;
const tileHeight = tileWidth / trickplayInfo.aspectRatio!;
+ const timeStr = `${time.hours > 0 ? `${time.hours}:` : ""}${
+ time.minutes < 10 ? `0${time.minutes}` : time.minutes
+ }:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`;
+
+ const finalScale = BASE_IMAGE_SCALE * imageScale;
return (
= ({
width: tileWidth,
height: tileHeight,
alignSelf: "center",
- transform: [{ scale: 1.4 }],
+ transform: [{ scale: finalScale }],
borderRadius: 5,
}}
className='bg-neutral-800 overflow-hidden'
>
= ({
source={{ uri: url }}
contentFit='cover'
/>
+ {/*
+ * Bottom-right overlay (Jellyfin web style) — chapter name (small,
+ * faded) above the timestamp (small, bold). Sits on top of the
+ * trickplay frame inside the same overflow:hidden container so it
+ * always stays within the bubble bounds.
+ */}
+
+ {chapterName ? (
+
+ {chapterName}
+
+ ) : null}
+
+ {timeStr}
+
+
-
- {`${time.hours > 0 ? `${time.hours}:` : ""}${
- time.minutes < 10 ? `0${time.minutes}` : time.minutes
- }:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`}
-
);
};
diff --git a/components/video-player/controls/constants.ts b/components/video-player/controls/constants.ts
index dc735cf95..cec24162a 100644
--- a/components/video-player/controls/constants.ts
+++ b/components/video-player/controls/constants.ts
@@ -1,12 +1,13 @@
export const CONTROLS_CONSTANTS = {
TIMEOUT: 4000,
- SCRUB_INTERVAL_MS: 10 * 1000, // 10 seconds in ms
+ SCRUB_INTERVAL_MS: 30 * 1000, // 30 seconds in ms
SCRUB_INTERVAL_TICKS: 10 * 10000000, // 10 seconds in ticks
TILE_WIDTH: 150,
PROGRESS_UNIT_MS: 1000, // 1 second in ms
PROGRESS_UNIT_TICKS: 10000000, // 1 second in ticks
- LONG_PRESS_INITIAL_SEEK: 10,
- LONG_PRESS_ACCELERATION: 1.1,
+ LONG_PRESS_INITIAL_SEEK: 30,
+ LONG_PRESS_ACCELERATION: 1.2,
+ LONG_PRESS_MAX_ACCELERATION: 4,
LONG_PRESS_INTERVAL: 300,
SLIDER_DEBOUNCE_MS: 3,
} as const;
diff --git a/components/video-player/controls/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx
index ec9ca995f..7c5750849 100644
--- a/components/video-player/controls/contexts/VideoContext.tsx
+++ b/components/video-player/controls/contexts/VideoContext.tsx
@@ -47,6 +47,7 @@
*/
import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
+import { File } from "expo-file-system";
import { useLocalSearchParams } from "expo-router";
import type React from "react";
import {
@@ -57,13 +58,19 @@ import {
useMemo,
useState,
} from "react";
+import { Platform } from "react-native";
import useRouter from "@/hooks/useAppRouter";
import type { MpvAudioTrack } from "@/modules";
import { useOfflineMode } from "@/providers/OfflineModeProvider";
+import { getSubtitlesForItem } from "@/utils/atoms/downloadedSubtitles";
import { isImageBasedSubtitle } from "@/utils/jellyfin/subtitleUtils";
import type { Track } from "../types";
import { usePlayerContext, usePlayerControls } from "./PlayerContext";
+// Starting index for local (client-downloaded) subtitles
+// Uses negative indices to avoid collision with Jellyfin indices
+const LOCAL_SUBTITLE_INDEX_START = -100;
+
interface VideoContextProps {
subtitleTracks: Track[] | null;
audioTracks: Track[] | null;
@@ -339,12 +346,40 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
};
});
+ // TV only: Merge locally downloaded subtitles (from OpenSubtitles)
+ if (Platform.isTV && itemId) {
+ const localSubs = getSubtitlesForItem(itemId);
+ let localIdx = 0;
+ for (const localSub of localSubs) {
+ // Verify file still exists (cache may have been cleared)
+ const subtitleFile = new File(localSub.filePath);
+ if (!subtitleFile.exists) {
+ continue;
+ }
+
+ const localIndex = LOCAL_SUBTITLE_INDEX_START - localIdx;
+ subs.push({
+ name: localSub.name,
+ index: localIndex,
+ mpvIndex: -1, // Will be loaded dynamically via addSubtitleFile
+ isLocal: true,
+ localPath: localSub.filePath,
+ setTrack: () => {
+ // Add the subtitle file to MPV and select it
+ playerControls.addSubtitleFile(localSub.filePath, true);
+ router.setParams({ subtitleIndex: String(localIndex) });
+ },
+ });
+ localIdx++;
+ }
+ }
+
setSubtitleTracks(subs.sort((a, b) => a.index - b.index));
setAudioTracks(audio);
};
fetchTracks();
- }, [tracksReady, mediaSource, offline, downloadedItem]);
+ }, [tracksReady, mediaSource, offline, downloadedItem, itemId]);
return (
diff --git a/components/video-player/controls/dropdown/DropdownView.tsx b/components/video-player/controls/dropdown/DropdownView.tsx
index 5b631ec48..7b6713b39 100644
--- a/components/video-player/controls/dropdown/DropdownView.tsx
+++ b/components/video-player/controls/dropdown/DropdownView.tsx
@@ -15,16 +15,18 @@ import { usePlayerContext } from "../contexts/PlayerContext";
import { useVideoContext } from "../contexts/VideoContext";
import { PlaybackSpeedScope } from "../utils/playback-speed-settings";
-// Subtitle size presets (stored as scale * 100, so 1.0 = 100)
-const SUBTITLE_SIZE_PRESETS = [
- { label: "0.5", value: 50 },
- { label: "0.6", value: 60 },
- { label: "0.7", value: 70 },
- { label: "0.8", value: 80 },
- { label: "0.9", value: 90 },
- { label: "1.0", value: 100 },
- { label: "1.1", value: 110 },
- { label: "1.2", value: 120 },
+// Subtitle scale presets (direct multiplier values)
+const SUBTITLE_SCALE_PRESETS = [
+ { label: "0.1x", value: 0.1 },
+ { label: "0.25x", value: 0.25 },
+ { label: "0.5x", value: 0.5 },
+ { label: "0.75x", value: 0.75 },
+ { label: "1.0x", value: 1.0 },
+ { label: "1.25x", value: 1.25 },
+ { label: "1.5x", value: 1.5 },
+ { label: "2.0x", value: 2.0 },
+ { label: "2.5x", value: 2.5 },
+ { label: "3.0x", value: 3.0 },
] as const;
interface DropdownViewProps {
@@ -124,15 +126,15 @@ const DropdownView = ({
})),
});
- // Subtitle Size Section
+ // Subtitle Scale Section
groups.push({
- title: "Subtitle Size",
- options: SUBTITLE_SIZE_PRESETS.map((preset) => ({
+ title: "Subtitle Scale",
+ options: SUBTITLE_SCALE_PRESETS.map((preset) => ({
type: "radio" as const,
label: preset.label,
value: preset.value.toString(),
- selected: settings.subtitleSize === preset.value,
- onPress: () => updateSettings({ subtitleSize: preset.value }),
+ selected: (settings.mpvSubtitleScale ?? 1.0) === preset.value,
+ onPress: () => updateSettings({ mpvSubtitleScale: preset.value }),
})),
});
}
@@ -190,7 +192,7 @@ const DropdownView = ({
audioTracksKey,
subtitleIndex,
audioIndex,
- settings.subtitleSize,
+ settings.mpvSubtitleScale,
updateSettings,
playbackSpeed,
setPlaybackSpeed,
diff --git a/components/video-player/controls/hooks/index.ts b/components/video-player/controls/hooks/index.ts
index 08b234ac5..cfb317599 100644
--- a/components/video-player/controls/hooks/index.ts
+++ b/components/video-player/controls/hooks/index.ts
@@ -1,3 +1,4 @@
+export { useChapterNavigation } from "./useChapterNavigation";
export { useRemoteControl } from "./useRemoteControl";
export { useVideoNavigation } from "./useVideoNavigation";
export { useVideoSlider } from "./useVideoSlider";
diff --git a/components/video-player/controls/hooks/useChapterNavigation.ts b/components/video-player/controls/hooks/useChapterNavigation.ts
new file mode 100644
index 000000000..00d3330c0
--- /dev/null
+++ b/components/video-player/controls/hooks/useChapterNavigation.ts
@@ -0,0 +1,150 @@
+import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client";
+import { useCallback, useMemo } from "react";
+import type { SharedValue } from "react-native-reanimated";
+import { ticksToMs } from "@/utils/time";
+
+export interface UseChapterNavigationProps {
+ /** Chapters array from the item */
+ chapters: ChapterInfo[] | null | undefined;
+ /** Current progress in milliseconds (SharedValue) */
+ progress: SharedValue;
+ /** Total duration in milliseconds */
+ maxMs: number;
+ /** Seek function that accepts milliseconds */
+ seek: (ms: number) => void;
+}
+
+export interface UseChapterNavigationReturn {
+ /** Array of chapters */
+ chapters: ChapterInfo[];
+ /** Index of the current chapter (-1 if no chapters) */
+ currentChapterIndex: number;
+ /** Current chapter info or null */
+ currentChapter: ChapterInfo | null;
+ /** Whether there's a next chapter available */
+ hasNextChapter: boolean;
+ /** Whether there's a previous chapter available */
+ hasPreviousChapter: boolean;
+ /** Navigate to the next chapter */
+ goToNextChapter: () => void;
+ /** Navigate to the previous chapter (or restart current if >3s in) */
+ goToPreviousChapter: () => void;
+ /** Array of chapter positions as percentages (0-100) for tick marks */
+ chapterPositions: number[];
+ /** Whether chapters are available */
+ hasChapters: boolean;
+}
+
+// Threshold in ms - if more than 3 seconds into chapter, restart instead of going to previous
+const RESTART_THRESHOLD_MS = 3000;
+
+/**
+ * Hook for chapter navigation in video player
+ * Provides current chapter info and navigation functions
+ */
+export function useChapterNavigation({
+ chapters: rawChapters,
+ progress,
+ maxMs,
+ seek,
+}: UseChapterNavigationProps): UseChapterNavigationReturn {
+ // Ensure chapters is always an array
+ const chapters = useMemo(() => rawChapters ?? [], [rawChapters]);
+
+ // Calculate chapter positions as percentages for tick marks
+ const chapterPositions = useMemo(() => {
+ if (!chapters.length || maxMs <= 0) return [];
+
+ return chapters
+ .map((chapter) => {
+ const positionMs = ticksToMs(chapter.StartPositionTicks);
+ return (positionMs / maxMs) * 100;
+ })
+ .filter((pos) => pos > 0 && pos < 100); // Skip first (0%) and any at the end
+ }, [chapters, maxMs]);
+
+ // Find current chapter index based on progress
+ // The current chapter is the one with the largest StartPositionTicks that is <= current progress
+ const getCurrentChapterIndex = useCallback((): number => {
+ if (!chapters.length) return -1;
+
+ const currentMs = progress.value;
+ let currentIndex = -1;
+
+ for (let i = 0; i < chapters.length; i++) {
+ const chapterMs = ticksToMs(chapters[i].StartPositionTicks);
+ if (chapterMs <= currentMs) {
+ currentIndex = i;
+ } else {
+ break;
+ }
+ }
+
+ return currentIndex;
+ }, [chapters, progress]);
+
+ // Current chapter index (computed once for rendering)
+ const currentChapterIndex = getCurrentChapterIndex();
+
+ // Current chapter info
+ const currentChapter = useMemo(() => {
+ if (currentChapterIndex < 0 || currentChapterIndex >= chapters.length) {
+ return null;
+ }
+ return chapters[currentChapterIndex];
+ }, [chapters, currentChapterIndex]);
+
+ // Navigation availability
+ const hasNextChapter =
+ chapters.length > 0 && currentChapterIndex < chapters.length - 1;
+ const hasPreviousChapter = chapters.length > 0 && currentChapterIndex >= 0;
+
+ // Navigate to next chapter
+ const goToNextChapter = useCallback(() => {
+ const idx = getCurrentChapterIndex();
+ if (idx < chapters.length - 1) {
+ const nextChapter = chapters[idx + 1];
+ const nextMs = ticksToMs(nextChapter.StartPositionTicks);
+ progress.value = nextMs;
+ seek(nextMs);
+ }
+ }, [chapters, getCurrentChapterIndex, progress, seek]);
+
+ // Navigate to previous chapter (or restart current if >3s in)
+ const goToPreviousChapter = useCallback(() => {
+ const idx = getCurrentChapterIndex();
+ if (idx < 0) return;
+
+ const currentChapterMs = ticksToMs(chapters[idx].StartPositionTicks);
+ const currentMs = progress.value;
+ const timeIntoChapter = currentMs - currentChapterMs;
+
+ // If more than 3 seconds into the current chapter, restart it
+ // Otherwise, go to the previous chapter
+ if (timeIntoChapter > RESTART_THRESHOLD_MS && idx >= 0) {
+ progress.value = currentChapterMs;
+ seek(currentChapterMs);
+ } else if (idx > 0) {
+ const prevChapter = chapters[idx - 1];
+ const prevMs = ticksToMs(prevChapter.StartPositionTicks);
+ progress.value = prevMs;
+ seek(prevMs);
+ } else {
+ // At the first chapter, just restart it
+ progress.value = currentChapterMs;
+ seek(currentChapterMs);
+ }
+ }, [chapters, getCurrentChapterIndex, progress, seek]);
+
+ return {
+ chapters,
+ currentChapterIndex,
+ currentChapter,
+ hasNextChapter,
+ hasPreviousChapter,
+ goToNextChapter,
+ goToPreviousChapter,
+ chapterPositions,
+ hasChapters: chapters.length > 0,
+ };
+}
diff --git a/components/video-player/controls/hooks/useRemoteControl.ts b/components/video-player/controls/hooks/useRemoteControl.ts
index 0a71b8b06..8a4fd902e 100644
--- a/components/video-player/controls/hooks/useRemoteControl.ts
+++ b/components/video-player/controls/hooks/useRemoteControl.ts
@@ -1,183 +1,237 @@
-import { useCallback, useEffect, useRef, useState } from "react";
-import { Platform } from "react-native";
+import { useEffect, useRef, useState } from "react";
+import { Alert } from "react-native";
import { type SharedValue, useSharedValue } from "react-native-reanimated";
-import { msToTicks, ticksToSeconds } from "@/utils/time";
-import { CONTROLS_CONSTANTS } from "../constants";
-
-// TV event handler with fallback for non-TV platforms
-let useTVEventHandler: (callback: (evt: any) => void) => void;
-if (Platform.isTV) {
- try {
- useTVEventHandler = require("react-native").useTVEventHandler;
- } catch {
- // Fallback for non-TV platforms
- useTVEventHandler = () => {};
- }
-} else {
- // No-op hook for non-TV platforms
- useTVEventHandler = () => {};
-}
+import { useTVBackPress } from "@/hooks/useTVBackPress";
+import { useTVEventHandler } from "@/hooks/useTVEventHandler";
interface UseRemoteControlProps {
- progress: SharedValue;
- min: SharedValue;
- max: SharedValue;
showControls: boolean;
- isPlaying: boolean;
- seek: (value: number) => void;
- play: () => void;
- togglePlay: () => void;
toggleControls: () => void;
- calculateTrickplayUrl: (progressInTicks: number) => void;
- handleSeekForward: (seconds: number) => void;
- handleSeekBackward: (seconds: number) => void;
+ /** When true, disables handling D-pad events (e.g., when settings modal is open) */
+ disableSeeking?: boolean;
+ /** Callback for back/menu button press (tvOS: menu, Android TV: back) */
+ onBack?: () => void;
+ /** Callback to hide controls (called on back press when controls are visible) */
+ onHideControls?: () => void;
+ /** Title of the video being played (shown in exit confirmation) */
+ videoTitle?: string;
+ /** Whether the progress bar currently has focus */
+ isProgressBarFocused?: boolean;
+ /** Callback for seeking left when progress bar is focused */
+ onSeekLeft?: () => void;
+ /** Callback for seeking right when progress bar is focused */
+ onSeekRight?: () => void;
+ /** Callback for seeking left when controls are hidden (minimal seek mode) */
+ onMinimalSeekLeft?: () => void;
+ /** Callback for seeking right when controls are hidden (minimal seek mode) */
+ onMinimalSeekRight?: () => void;
+ /** Callback for any interaction that should reset the controls timeout */
+ onInteraction?: () => void;
+ /** Callback when long press seek left starts (eventKeyAction: 0) */
+ onLongSeekLeftStart?: () => void;
+ /** Callback when long press seek right starts (eventKeyAction: 0) */
+ onLongSeekRightStart?: () => void;
+ /** Callback when long press seek ends (eventKeyAction: 1) */
+ onLongSeekStop?: () => void;
+ /** Callback when up/down D-pad pressed (to show controls with play button focused) */
+ onVerticalDpad?: () => void;
+ /** Called before the exit confirmation Alert is shown (e.g., to pause countdown) */
+ onWillExit?: () => void;
+ /** Called when the user cancels the exit confirmation Alert */
+ onCancelExit?: () => void;
+ // Legacy props - kept for backwards compatibility with mobile Controls.tsx
+ // These are ignored in the simplified implementation
+ progress?: SharedValue;
+ min?: SharedValue;
+ max?: SharedValue;
+ isPlaying?: boolean;
+ seek?: (value: number) => void;
+ play?: () => void;
+ togglePlay?: () => void;
+ calculateTrickplayUrl?: (progressInTicks: number) => void;
+ handleSeekForward?: (seconds: number) => void;
+ handleSeekBackward?: (seconds: number) => void;
}
/**
* Hook to manage TV remote control interactions.
- * MPV player uses milliseconds for time values.
+ * Simplified version - D-pad navigation is handled by native focus system.
+ * This hook handles:
+ * - Showing controls on any button press
+ * - Play/pause button on TV remote
*/
export function useRemoteControl({
- progress,
- min,
- max,
showControls,
- isPlaying,
- seek,
- play,
togglePlay,
- toggleControls,
- calculateTrickplayUrl,
- handleSeekForward,
- handleSeekBackward,
+ onBack,
+ onHideControls,
+ videoTitle,
+ isProgressBarFocused,
+ onSeekLeft,
+ onSeekRight,
+ onMinimalSeekLeft,
+ onMinimalSeekRight,
+ onInteraction,
+ onLongSeekLeftStart,
+ onLongSeekRightStart,
+ onLongSeekStop,
+ onVerticalDpad,
+ onWillExit,
+ onCancelExit,
}: UseRemoteControlProps) {
+ // Keep these for backward compatibility with the component
const remoteScrubProgress = useSharedValue(null);
const isRemoteScrubbing = useSharedValue(false);
- const [showRemoteBubble, setShowRemoteBubble] = useState(false);
- const [longPressScrubMode, setLongPressScrubMode] = useState<
- "FF" | "RW" | null
- >(null);
- const [isSliding, setIsSliding] = useState(false);
- const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 });
+ const [showRemoteBubble] = useState(false);
+ const [isSliding] = useState(false);
+ const [time] = useState({ hours: 0, minutes: 0, seconds: 0 });
- const longPressTimeoutRef = useRef | null>(
- null,
- );
- // MPV uses ms
- const SCRUB_INTERVAL = CONTROLS_CONSTANTS.SCRUB_INTERVAL_MS;
+ // Use refs to avoid stale closures in BackHandler
+ const showControlsRef = useRef(showControls);
+ const onHideControlsRef = useRef(onHideControls);
+ const onBackRef = useRef(onBack);
+ const videoTitleRef = useRef(videoTitle);
+ const onWillExitRef = useRef(onWillExit);
+ const onCancelExitRef = useRef(onCancelExit);
- const updateTime = useCallback((progressValue: number) => {
- // Convert ms to ticks for calculation
- const progressInTicks = msToTicks(progressValue);
- const progressInSeconds = Math.floor(ticksToSeconds(progressInTicks));
- const hours = Math.floor(progressInSeconds / 3600);
- const minutes = Math.floor((progressInSeconds % 3600) / 60);
- const seconds = progressInSeconds % 60;
- setTime({ hours, minutes, seconds });
+ useEffect(() => {
+ showControlsRef.current = showControls;
+ onHideControlsRef.current = onHideControls;
+ onBackRef.current = onBack;
+ videoTitleRef.current = videoTitle;
+ onWillExitRef.current = onWillExit;
+ onCancelExitRef.current = onCancelExit;
+ }, [
+ showControls,
+ onHideControls,
+ onBack,
+ videoTitle,
+ onWillExit,
+ onCancelExit,
+ ]);
+
+ // BackHandler owns player exit: Android TV sends hardware back here, and
+ // react-native-tvos maps the Apple TV menu button to the same API.
+ useTVBackPress(() => {
+ if (showControlsRef.current && onHideControlsRef.current) {
+ // Controls are visible, so the first back press only hides them.
+ onHideControlsRef.current();
+ return true;
+ }
+ if (onBackRef.current) {
+ // Signal Controls that exit is imminent (pauses countdown, sets guard)
+ onWillExitRef.current?.();
+
+ // Controls are hidden, so confirm before leaving playback.
+ Alert.alert(
+ "Stop Playback",
+ videoTitleRef.current
+ ? `Stop playing "${videoTitleRef.current}"?`
+ : "Are you sure you want to stop playback?",
+ [
+ {
+ text: "Cancel",
+ style: "cancel",
+ onPress: () => onCancelExitRef.current?.(),
+ },
+ { text: "Stop", style: "destructive", onPress: onBackRef.current },
+ ],
+ );
+ return true;
+ }
+ return false;
}, []);
// TV remote control handling (no-op on non-TV platforms)
useTVEventHandler((evt) => {
if (!evt) return;
- switch (evt.eventType) {
- case "longLeft": {
- setLongPressScrubMode((prev) => (!prev ? "RW" : null));
- break;
- }
- case "longRight": {
- setLongPressScrubMode((prev) => (!prev ? "FF" : null));
- break;
- }
- case "left":
- case "right": {
- isRemoteScrubbing.value = true;
- setShowRemoteBubble(true);
-
- const direction = evt.eventType === "left" ? -1 : 1;
- const base = remoteScrubProgress.value ?? progress.value;
- const updated = Math.max(
- min.value,
- Math.min(max.value, base + direction * SCRUB_INTERVAL),
- );
- remoteScrubProgress.value = updated;
- // Convert ms to ticks for trickplay
- const progressInTicks = msToTicks(updated);
- calculateTrickplayUrl(progressInTicks);
- updateTime(updated);
- break;
- }
- case "select": {
- if (isRemoteScrubbing.value && remoteScrubProgress.value != null) {
- progress.value = remoteScrubProgress.value;
-
- // MPV uses ms, seek expects ms
- const seekTarget = Math.max(0, remoteScrubProgress.value);
-
- seek(seekTarget);
- if (isPlaying) play();
-
- isRemoteScrubbing.value = false;
- remoteScrubProgress.value = null;
- setShowRemoteBubble(false);
- } else {
- togglePlay();
- }
- break;
- }
- case "down":
- case "up":
- // cancel scrubbing on other directions
- isRemoteScrubbing.value = false;
- remoteScrubProgress.value = null;
- setShowRemoteBubble(false);
- break;
- default:
- break;
+ // Back/menu is handled by useTVBackPress above. Keep this handler focused
+ // on remote-control events like play/pause, D-pad, and long seek.
+ if (evt.eventType === "menu") {
+ return;
}
- if (!showControls) toggleControls();
+ // Handle play/pause button press on TV remote
+ if (evt.eventType === "playPause") {
+ togglePlay?.();
+ onInteraction?.();
+ return;
+ }
+
+ // Handle long press D-pad for continuous seeking (works in both modes)
+ // Must be checked BEFORE the showControls check to work when controls are hidden
+ if (evt.eventType === "longLeft") {
+ if (evt.eventKeyAction === 0 && onLongSeekLeftStart) {
+ // Key pressed - start continuous seeking backward
+ onLongSeekLeftStart();
+ } else if (evt.eventKeyAction === 1 && onLongSeekStop) {
+ // Key released - stop seeking
+ onLongSeekStop();
+ }
+ return;
+ }
+
+ if (evt.eventType === "longRight") {
+ if (evt.eventKeyAction === 0 && onLongSeekRightStart) {
+ // Key pressed - start continuous seeking forward
+ onLongSeekRightStart();
+ } else if (evt.eventKeyAction === 1 && onLongSeekStop) {
+ // Key released - stop seeking
+ onLongSeekStop();
+ }
+ return;
+ }
+
+ // Handle D-pad when controls are hidden
+ if (!showControls) {
+ // Ignore select/enter events - let the native Pressable handle them
+ // This prevents controls from showing when pressing buttons like skip intro
+ if (evt.eventType === "select" || evt.eventType === "enter") {
+ return;
+ }
+ // Minimal seek mode for left/right
+ if (evt.eventType === "left" && onMinimalSeekLeft) {
+ onMinimalSeekLeft();
+ return;
+ }
+ if (evt.eventType === "right" && onMinimalSeekRight) {
+ onMinimalSeekRight();
+ return;
+ }
+ // Up/down shows controls with play button focused
+ if (
+ (evt.eventType === "up" || evt.eventType === "down") &&
+ onVerticalDpad
+ ) {
+ onVerticalDpad();
+ return;
+ }
+ // Ignore all other events (focus/blur, swipes, etc.)
+ // User can press up/down to show controls
+ return;
+ }
+
+ // Controls are showing - handle seeking when progress bar is focused
+ if (isProgressBarFocused) {
+ if (evt.eventType === "left" && onSeekLeft) {
+ onSeekLeft();
+ return;
+ }
+ if (evt.eventType === "right" && onSeekRight) {
+ onSeekRight();
+ return;
+ }
+ }
+
+ // Reset the timeout on any D-pad navigation when controls are showing
+ onInteraction?.();
});
- useEffect(() => {
- let isActive = true;
- let seekTime = CONTROLS_CONSTANTS.LONG_PRESS_INITIAL_SEEK;
-
- const scrubWithLongPress = () => {
- if (!isActive || !longPressScrubMode) return;
-
- setIsSliding(true);
- const scrubFn =
- longPressScrubMode === "FF" ? handleSeekForward : handleSeekBackward;
- scrubFn(seekTime);
- seekTime *= CONTROLS_CONSTANTS.LONG_PRESS_ACCELERATION;
-
- longPressTimeoutRef.current = setTimeout(
- scrubWithLongPress,
- CONTROLS_CONSTANTS.LONG_PRESS_INTERVAL,
- );
- };
-
- if (longPressScrubMode) {
- isActive = true;
- scrubWithLongPress();
- }
-
- return () => {
- isActive = false;
- setIsSliding(false);
- if (longPressTimeoutRef.current) {
- clearTimeout(longPressTimeoutRef.current);
- longPressTimeoutRef.current = null;
- }
- };
- }, [longPressScrubMode, handleSeekForward, handleSeekBackward]);
-
return {
remoteScrubProgress,
isRemoteScrubbing,
showRemoteBubble,
- longPressScrubMode,
isSliding,
time,
};
diff --git a/components/video-player/controls/hooks/useVideoSlider.ts b/components/video-player/controls/hooks/useVideoSlider.ts
index dfc1164bb..3c19ce7ad 100644
--- a/components/video-player/controls/hooks/useVideoSlider.ts
+++ b/components/video-player/controls/hooks/useVideoSlider.ts
@@ -74,6 +74,21 @@ export function useVideoSlider({
[seek, play, progress, isSeeking],
);
+ // Programmatic seek (chapter list, hotkeys) that bypasses the slide gesture.
+ // Reads `isPlaying` directly instead of `wasPlayingRef`, which is only set
+ // during a real slide and would carry stale state on a tap-to-seek.
+ const seekTo = useCallback(
+ (value: number) => {
+ const seekValue = Math.max(0, Math.floor(value));
+ progress.value = seekValue;
+ seek(seekValue);
+ if (isPlaying) {
+ play();
+ }
+ },
+ [seek, play, progress, isPlaying],
+ );
+
const handleSliderChange = useCallback(
debounce((value: number) => {
// Convert ms to ticks for trickplay
@@ -96,5 +111,6 @@ export function useVideoSlider({
handleTouchEnd,
handleSliderComplete,
handleSliderChange,
+ seekTo,
};
}
diff --git a/components/video-player/controls/types.ts b/components/video-player/controls/types.ts
index 5ec03eddb..ca2ea1413 100644
--- a/components/video-player/controls/types.ts
+++ b/components/video-player/controls/types.ts
@@ -22,6 +22,10 @@ type Track = {
index: number;
mpvIndex?: number;
setTrack: () => void;
+ /** True for client-side downloaded subtitles (e.g., from OpenSubtitles) */
+ isLocal?: boolean;
+ /** File path for local subtitles */
+ localPath?: string;
};
-export type { EmbeddedSubtitle, ExternalSubtitle, TranscodedSubtitle, Track };
+export type { EmbeddedSubtitle, ExternalSubtitle, Track, TranscodedSubtitle };
diff --git a/constants/TVPosterSizes.ts b/constants/TVPosterSizes.ts
new file mode 100644
index 000000000..132b75a66
--- /dev/null
+++ b/constants/TVPosterSizes.ts
@@ -0,0 +1,12 @@
+/**
+ * @deprecated Import from "@/constants/TVSizes" instead.
+ * This file is kept for backwards compatibility.
+ */
+
+export {
+ type ScaledTVPosterSizes,
+ TVPosterSizes,
+ useScaledTVPosterSizes,
+} from "./TVSizes";
+
+export type TVPosterSizeKey = keyof typeof import("./TVSizes").TVPosterSizes;
diff --git a/constants/TVSizes.ts b/constants/TVSizes.ts
new file mode 100644
index 000000000..117609021
--- /dev/null
+++ b/constants/TVSizes.ts
@@ -0,0 +1,179 @@
+import { TVTypographyScale, useSettings } from "@/utils/atoms/settings";
+import { scaleSize } from "@/utils/scaleSize";
+
+/**
+ * TV Layout Sizes
+ *
+ * Unified constants for TV interface layout including posters, gaps, and padding.
+ * Base values are designed for 1920x1080 and scaled to the actual viewport via
+ * scaleSize(), then further adjusted by the user's tvTypographyScale setting.
+ */
+
+// =============================================================================
+// BASE VALUES (at Default scale)
+// =============================================================================
+
+/**
+ * Base poster widths in pixels.
+ * Heights are calculated from aspect ratios.
+ */
+export const TVPosterSizes = {
+ /** Portrait posters (movies, series) - 10:15 aspect ratio */
+ poster: 300,
+
+ /** Landscape posters (continue watching, thumbs, hero) - 16:9 aspect ratio */
+ landscape: 470,
+
+ /** Episode cards - 16:9 aspect ratio */
+ episode: 440,
+} as const;
+
+/**
+ * Base gap/spacing values in pixels.
+ */
+export const TVGaps = {
+ /** Gap between items in horizontal lists */
+ item: 24,
+
+ /** Gap between sections vertically */
+ section: 32,
+
+ /** Small gap for tight layouts */
+ small: 12,
+
+ /** Large gap for spacious layouts */
+ large: 48,
+} as const;
+
+/**
+ * Base padding values in pixels.
+ */
+export const TVPadding = {
+ /** Horizontal padding from screen edges (static — matches native search inset) */
+ horizontal: 80,
+
+ /** Padding to accommodate scale animations (1.05x) */
+ scale: 20,
+
+ /** Vertical padding for content areas */
+ vertical: 24,
+
+ /** Hero section height as percentage of screen height (0.0 - 1.0) */
+ heroHeight: 0.6,
+} as const;
+
+/**
+ * Animation and interaction values.
+ */
+export const TVAnimation = {
+ /** Scale factor for focused items */
+ focusScale: 1.05,
+} as const;
+
+// =============================================================================
+// SCALING
+// =============================================================================
+
+/**
+ * Scale multipliers for each typography scale level.
+ * Applied to poster sizes and gaps.
+ */
+const sizeScaleMultipliers: Record = {
+ [TVTypographyScale.Small]: 0.53,
+ [TVTypographyScale.Default]: 0.63,
+ [TVTypographyScale.Large]: 0.77,
+ [TVTypographyScale.ExtraLarge]: 0.84,
+};
+
+// =============================================================================
+// HOOKS
+// =============================================================================
+
+export type ScaledTVPosterSizes = {
+ poster: number;
+ landscape: number;
+ episode: number;
+};
+
+export type ScaledTVGaps = {
+ item: number;
+ section: number;
+ small: number;
+ large: number;
+};
+
+export type ScaledTVPadding = {
+ horizontal: number;
+ scale: number;
+ vertical: number;
+ heroHeight: number;
+};
+
+export type ScaledTVSizes = {
+ posters: ScaledTVPosterSizes;
+ gaps: ScaledTVGaps;
+ padding: ScaledTVPadding;
+ animation: typeof TVAnimation;
+};
+
+/**
+ * Hook that returns all scaled TV sizes based on user settings.
+ *
+ * @example
+ * const sizes = useScaledTVSizes();
+ *
+ */
+export const useScaledTVSizes = (): ScaledTVSizes => {
+ const { settings } = useSettings();
+ const scale =
+ sizeScaleMultipliers[settings.tvTypographyScale] ??
+ sizeScaleMultipliers[TVTypographyScale.Default];
+
+ return {
+ posters: {
+ poster: Math.round(scaleSize(TVPosterSizes.poster) * scale),
+ landscape: Math.round(scaleSize(TVPosterSizes.landscape) * scale),
+ episode: Math.round(scaleSize(TVPosterSizes.episode) * scale),
+ },
+ gaps: {
+ item: Math.round(scaleSize(TVGaps.item) * scale),
+ section: Math.round(scaleSize(TVGaps.section) * scale),
+ small: Math.round(scaleSize(TVGaps.small) * scale),
+ large: Math.round(scaleSize(TVGaps.large) * scale),
+ },
+ padding: {
+ // Static: matches the native tvOS search bar inset, which is a fixed
+ // point value and does not change with the typography scale setting.
+ horizontal: TVPadding.horizontal,
+ scale: Math.round(scaleSize(TVPadding.scale) * scale),
+ vertical: Math.round(scaleSize(TVPadding.vertical) * scale),
+ heroHeight: TVPadding.heroHeight * scale,
+ },
+ animation: TVAnimation,
+ };
+};
+
+/**
+ * Hook that returns only scaled poster sizes.
+ * Use this for backwards compatibility or when you only need poster sizes.
+ */
+export const useScaledTVPosterSizes = (): ScaledTVPosterSizes => {
+ const sizes = useScaledTVSizes();
+ return sizes.posters;
+};
+
+/**
+ * Hook that returns only scaled gap sizes.
+ */
+export const useScaledTVGaps = (): ScaledTVGaps => {
+ const sizes = useScaledTVSizes();
+ return sizes.gaps;
+};
+
+/**
+ * Hook that returns only scaled padding sizes.
+ */
+export const useScaledTVPadding = (): ScaledTVPadding => {
+ const sizes = useScaledTVSizes();
+ return sizes.padding;
+};
diff --git a/constants/TVTypography.ts b/constants/TVTypography.ts
new file mode 100644
index 000000000..cbac9b693
--- /dev/null
+++ b/constants/TVTypography.ts
@@ -0,0 +1,75 @@
+import { TVTypographyScale, useSettings } from "@/utils/atoms/settings";
+import { scaleSize } from "@/utils/scaleSize";
+
+/**
+ * TV Typography Scale
+ *
+ * Consistent text sizes for TV interface components.
+ * Base values are designed for 1920x1080 and scaled to the actual viewport via
+ * scaleSize(), then further adjusted by the user's tvTypographyScale setting.
+ */
+
+// =============================================================================
+// BASE VALUES (at Default scale)
+// =============================================================================
+
+export const TVTypography = {
+ /** Hero titles, movie/show names */
+ display: 70,
+
+ /** Episode series name, major headings */
+ title: 42,
+
+ /** Section headers (Cast, Technical Details, From this Series) */
+ heading: 32,
+
+ /** Overview, actor names, card titles, metadata */
+ body: 40,
+
+ /** Secondary text, labels, subtitles */
+ callout: 26,
+};
+
+export type TVTypographyKey = keyof typeof TVTypography;
+
+// =============================================================================
+// SCALING
+// =============================================================================
+
+const scaleMultipliers: Record = {
+ [TVTypographyScale.Small]: 0.6,
+ [TVTypographyScale.Default]: 0.7,
+ [TVTypographyScale.Large]: 0.84,
+ [TVTypographyScale.ExtraLarge]: 0.98,
+};
+
+// =============================================================================
+// HOOKS
+// =============================================================================
+
+export type ScaledTVTypography = {
+ display: number;
+ title: number;
+ heading: number;
+ body: number;
+ callout: number;
+};
+
+/**
+ * Hook that returns scaled TV typography values based on user settings.
+ * Use this instead of the static TVTypography constant for dynamic scaling.
+ */
+export const useScaledTVTypography = (): ScaledTVTypography => {
+ const { settings } = useSettings();
+ const scale =
+ scaleMultipliers[settings.tvTypographyScale] ??
+ scaleMultipliers[TVTypographyScale.Default];
+
+ return {
+ display: Math.round(scaleSize(TVTypography.display) * scale),
+ title: Math.round(scaleSize(TVTypography.title) * scale),
+ heading: Math.round(scaleSize(TVTypography.heading) * scale),
+ body: Math.round(scaleSize(TVTypography.body) * scale),
+ callout: Math.round(scaleSize(TVTypography.callout) * scale),
+ };
+};
diff --git a/docs/research/hdr-mpv.md b/docs/research/hdr-mpv.md
new file mode 100644
index 000000000..6061e026d
--- /dev/null
+++ b/docs/research/hdr-mpv.md
@@ -0,0 +1,436 @@
+# HDR Support on tvOS with mpv - Research Document
+
+## Problem Statement
+
+HDR content appears washed out on Apple TV when using the mpv-based player. The TV doesn't show an HDR indicator and colors look flat compared to other apps like Infuse.
+
+**Key Discovery:** HDR works correctly on iPhone but not on tvOS, despite using the same mpv player.
+
+---
+
+## Why HDR Works on iPhone
+
+In `MpvPlayerView.swift`:
+```swift
+#if !os(tvOS)
+if #available(iOS 17.0, *) {
+ displayLayer.wantsExtendedDynamicRangeContent = true
+}
+#endif
+```
+
+On iOS 17+, setting `wantsExtendedDynamicRangeContent = true` on `AVSampleBufferDisplayLayer` enables Extended Dynamic Range (EDR). This tells the display layer to preserve HDR metadata and render in high dynamic range.
+
+**This API does not exist on tvOS.** Attempting to use it results in:
+> 'wantsExtendedDynamicRangeContent' is unavailable in tvOS
+
+tvOS uses a different HDR architecture designed for external displays via HDMI.
+
+---
+
+## tvOS HDR Architecture
+
+Unlike iPhone (integrated display), Apple TV connects to external TVs. Apple expects apps to:
+
+1. **Use `AVDisplayCriteria`** to request display mode changes
+2. **Attach proper colorspace metadata** to pixel buffers
+3. **Let the TV handle HDR rendering** via HDMI passthrough
+
+This is how Netflix, Infuse, and the TV app work - they signal "I'm playing HDR10 at 24fps" and tvOS switches the TV to that mode.
+
+---
+
+## MPVKit vo_avfoundation Analysis
+
+**Location:** `/MPVKit/Sources/BuildScripts/patch/libmpv/0004-avfoundation-video-output.patch`
+
+### Existing HDR Infrastructure
+
+The driver has comprehensive HDR support already built in:
+
+#### 1. HDR Metadata Copy Function (lines 253-270)
+```c
+static void copy_hdr_metadata(CVPixelBufferRef src, CVPixelBufferRef dst)
+{
+ const CFStringRef keys[] = {
+ kCVImageBufferTransferFunctionKey, // PQ for HDR10, HLG for HLG
+ kCVImageBufferColorPrimariesKey, // BT.2020 for HDR
+ kCVImageBufferYCbCrMatrixKey,
+ kCVImageBufferMasteringDisplayColorVolumeKey, // HDR10 static metadata
+ kCVImageBufferContentLightLevelInfoKey, // MaxCLL, MaxFALL
+ };
+
+ for (size_t i = 0; i < MP_ARRAY_SIZE(keys); i++) {
+ CFTypeRef value = CVBufferGetAttachment(src, keys[i], NULL);
+ if (value) {
+ CVBufferSetAttachment(dst, keys[i], value, kCVAttachmentMode_ShouldPropagate);
+ }
+ }
+}
+```
+
+#### 2. 10-bit HDR Format Support (lines 232-247)
+```c
+// For 10-bit HDR content (P010), use RGBA half-float to preserve HDR precision
+if (format == kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange ||
+ format == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange) {
+ outputFormat = kCVPixelFormatType_64RGBAHalf;
+}
+```
+
+#### 3. HDR-Safe GPU Compositing (lines 694-695)
+```c
+CGColorSpaceRef workingColorSpace = CGColorSpaceCreateWithName(
+ kCGColorSpaceExtendedLinearDisplayP3);
+```
+
+### The Problem: Metadata Not Attached in Main Code Path
+
+**Critical Finding:** `copy_hdr_metadata()` is only called during OSD compositing (line 609-610):
+
+```c
+// In composite mode, render OSD and composite onto frame
+if (p->composite_osd) {
+ render_osd(vo, pts);
+ CVPixelBufferRef composited = composite_frame(vo, pixbuf);
+ // copy_hdr_metadata() called inside composite_frame()
+}
+```
+
+If `composite_osd` is false (default), **HDR metadata is never attached**.
+
+### Frame Flow Analysis
+
+```
+draw_frame() called
+ │
+ ├─► Hardware decoded (IMGFMT_VIDEOTOOLBOX)
+ │ └─► pixbuf = mpi->planes[3] // Direct from VideoToolbox
+ │ └─► Metadata SHOULD be attached by decoder, but not verified
+ │
+ └─► Software decoded (NV12, 420P, P010)
+ └─► upload_software_frame()
+ └─► Creates new CVPixelBuffer
+ └─► Copies pixel data only
+ └─► ❌ NO colorspace metadata attached!
+
+ ▼
+ CMVideoFormatDescriptionCreateForImageBuffer(finalBuffer)
+ └─► Format description created FROM pixel buffer
+ └─► If buffer lacks HDR metadata, format won't have it
+
+ ▼
+ [displayLayer enqueueSampleBuffer:buf]
+ └─► Sent to display layer without HDR signal
+```
+
+---
+
+## Root Cause Summary
+
+| Issue | Impact |
+|-------|--------|
+| `wantsExtendedDynamicRangeContent` unavailable on tvOS | Can't use iOS EDR approach |
+| `copy_hdr_metadata()` only runs during OSD compositing | Main playback path skips HDR metadata |
+| Software decoded frames get no colorspace attachments | mpv knows colorspace but doesn't pass it to pixel buffer |
+| VideoToolbox metadata not verified | May or may not have HDR attachments |
+
+---
+
+## mp_image Colorspace Structures
+
+mpv uses libplacebo's colorspace structures. Here's how colorspace info flows:
+
+### Structure Hierarchy
+
+```
+mp_image (video/mp_image.h)
+ └─► params: mp_image_params
+ └─► color: pl_color_space
+ │ ├─► primaries: pl_color_primaries (BT.2020, etc.)
+ │ ├─► transfer: pl_color_transfer (PQ, HLG, etc.)
+ │ └─► hdr: pl_hdr_metadata (MaxCLL, MaxFALL, etc.)
+ └─► repr: pl_color_repr
+ ├─► sys: pl_color_system (BT.2100_PQ, etc.)
+ └─► levels: pl_color_levels (TV/Full range)
+```
+
+### Key Enums
+
+#### Color Primaries (`enum pl_color_primaries`)
+```c
+PL_COLOR_PRIM_UNKNOWN = 0,
+PL_COLOR_PRIM_BT_709, // HD/SDR standard
+PL_COLOR_PRIM_BT_2020, // UHD/HDR wide gamut ← HDR
+PL_COLOR_PRIM_DCI_P3, // DCI P3 (cinema)
+PL_COLOR_PRIM_DISPLAY_P3, // Display P3 (Apple)
+// ... more
+```
+
+#### Transfer Functions (`enum pl_color_transfer`)
+```c
+PL_COLOR_TRC_UNKNOWN = 0,
+PL_COLOR_TRC_BT_1886, // SDR gamma
+PL_COLOR_TRC_SRGB, // sRGB
+PL_COLOR_TRC_PQ, // SMPTE 2084 PQ (HDR10/DolbyVision) ← HDR
+PL_COLOR_TRC_HLG, // ITU-R BT.2100 HLG ← HDR
+// ... more
+```
+
+#### Color Systems (`enum pl_color_system`)
+```c
+PL_COLOR_SYSTEM_BT_709, // HD/SDR
+PL_COLOR_SYSTEM_BT_2020_NC, // UHD (non-constant luminance)
+PL_COLOR_SYSTEM_BT_2100_PQ, // HDR10 ← HDR
+PL_COLOR_SYSTEM_BT_2100_HLG, // HLG ← HDR
+PL_COLOR_SYSTEM_DOLBYVISION, // Dolby Vision ← HDR
+// ... more
+```
+
+### HDR Metadata Structure (`struct pl_hdr_metadata`)
+```c
+struct pl_hdr_metadata {
+ struct pl_raw_primaries prim; // CIE xy primaries
+ float min_luma, max_luma; // Luminance range (cd/m²)
+ float max_cll; // Maximum Content Light Level
+ float max_fall; // Maximum Frame-Average Light Level
+ // ... more
+};
+```
+
+### Accessing Colorspace in vo_avfoundation
+
+```c
+// In draw_frame():
+struct mp_image *mpi = frame->current;
+
+// Color primaries
+enum pl_color_primaries prim = mpi->params.color.primaries;
+
+// Transfer function
+enum pl_color_transfer trc = mpi->params.color.transfer;
+
+// HDR metadata
+struct pl_hdr_metadata hdr = mpi->params.color.hdr;
+
+// HDR detection
+bool is_hdr = (trc == PL_COLOR_TRC_PQ || trc == PL_COLOR_TRC_HLG);
+bool is_wide_gamut = (prim == PL_COLOR_PRIM_BT_2020);
+```
+
+---
+
+## The Fix
+
+### Required Changes in vo_avfoundation
+
+**File:** `MPVKit/Sources/BuildScripts/patch/libmpv/0004-avfoundation-video-output.patch`
+
+**Location:** After line 821 in `draw_frame()`, before the sample buffer is created.
+
+#### Add Required Include
+```c
+#include "video/csputils.h" // For pl_color_* enums (if not already included)
+```
+
+#### Add HDR Metadata Attachment Function
+```c
+// Add after copy_hdr_metadata() function (around line 270)
+static void attach_hdr_metadata(struct vo *vo, CVPixelBufferRef pixbuf,
+ struct mp_image *mpi)
+{
+ enum pl_color_primaries prim = mpi->params.color.primaries;
+ enum pl_color_transfer trc = mpi->params.color.transfer;
+
+ // Attach BT.2020 color primaries (HDR wide color gamut)
+ if (prim == PL_COLOR_PRIM_BT_2020) {
+ CVBufferSetAttachment(pixbuf, kCVImageBufferColorPrimariesKey,
+ kCVImageBufferColorPrimaries_ITU_R_2020,
+ kCVAttachmentMode_ShouldPropagate);
+ CVBufferSetAttachment(pixbuf, kCVImageBufferYCbCrMatrixKey,
+ kCVImageBufferYCbCrMatrix_ITU_R_2020,
+ kCVAttachmentMode_ShouldPropagate);
+
+ MP_VERBOSE(vo, "HDR: Attached BT.2020 color primaries\n");
+ }
+
+ // Attach PQ transfer function (HDR10/Dolby Vision)
+ if (trc == PL_COLOR_TRC_PQ) {
+ CVBufferSetAttachment(pixbuf, kCVImageBufferTransferFunctionKey,
+ kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ,
+ kCVAttachmentMode_ShouldPropagate);
+
+ MP_VERBOSE(vo, "HDR: Attached PQ transfer function (HDR10)\n");
+ }
+ // Attach HLG transfer function
+ else if (trc == PL_COLOR_TRC_HLG) {
+ CVBufferSetAttachment(pixbuf, kCVImageBufferTransferFunctionKey,
+ kCVImageBufferTransferFunction_ITU_R_2100_HLG,
+ kCVAttachmentMode_ShouldPropagate);
+
+ MP_VERBOSE(vo, "HDR: Attached HLG transfer function\n");
+ }
+
+ // Attach HDR static metadata if available
+ struct pl_hdr_metadata hdr = mpi->params.color.hdr;
+ if (hdr.max_cll > 0 || hdr.max_fall > 0) {
+ // ContentLightLevelInfo is a 4-byte structure:
+ // - 2 bytes: MaxCLL (max content light level)
+ // - 2 bytes: MaxFALL (max frame-average light level)
+ uint16_t cll_data[2] = {
+ (uint16_t)fminf(hdr.max_cll, 65535.0f),
+ (uint16_t)fminf(hdr.max_fall, 65535.0f)
+ };
+
+ CFDataRef cllInfo = CFDataCreate(NULL, (const UInt8 *)cll_data, sizeof(cll_data));
+ if (cllInfo) {
+ CVBufferSetAttachment(pixbuf, kCVImageBufferContentLightLevelInfoKey,
+ cllInfo, kCVAttachmentMode_ShouldPropagate);
+ CFRelease(cllInfo);
+
+ MP_VERBOSE(vo, "HDR: Attached CLL metadata (MaxCLL=%d, MaxFALL=%d)\n",
+ cll_data[0], cll_data[1]);
+ }
+ }
+}
+```
+
+#### Call the Function in draw_frame()
+
+```c
+// In draw_frame(), after line 821 (after getting pixbuf), add:
+
+// Attach HDR colorspace metadata to pixel buffer
+// This ensures the display layer receives proper HDR signaling
+attach_hdr_metadata(vo, pixbuf, mpi);
+```
+
+### Complete draw_frame() Modification
+
+The modified section should look like:
+
+```c
+CVPixelBufferRef pixbuf = NULL;
+bool pixbufNeedsRelease = false;
+
+// Handle different input formats
+if (mpi->imgfmt == IMGFMT_VIDEOTOOLBOX) {
+ // Hardware decoded: zero-copy passthrough
+ pixbuf = (CVPixelBufferRef)mpi->planes[3];
+} else {
+ // Software decoded: upload to CVPixelBuffer
+ pixbuf = upload_software_frame(vo, mpi);
+ if (!pixbuf) {
+ MP_ERR(vo, "Failed to upload software frame\n");
+ mp_image_unrefp(&mpi);
+ return false;
+ }
+ pixbufNeedsRelease = true;
+}
+
+// >>> NEW: Attach HDR colorspace metadata <<<
+attach_hdr_metadata(vo, pixbuf, mpi);
+
+CVPixelBufferRef finalBuffer = pixbuf;
+bool needsRelease = false;
+// ... rest of the function
+```
+
+---
+
+## Alternative Solutions
+
+### Option A: Enable composite_osd Mode (Quick Test)
+
+Since `copy_hdr_metadata()` works in composite mode, try enabling it:
+```
+--avfoundation-composite-osd=yes
+```
+
+This would trigger the existing HDR metadata path. Downside: OSD compositing has performance overhead.
+
+### Option B: Full vo_avfoundation Fix (Recommended)
+
+Modify the driver to always attach colorspace metadata based on `mp_image` params. This is the implementation described above.
+
+### Option C: Dual Player Approach
+
+Use AVPlayer for HDR content, mpv for everything else. This is what Swiftfin does.
+
+---
+
+## Implementation Checklist
+
+- [ ] Clone MPVKit fork
+- [ ] Modify `0004-avfoundation-video-output.patch`:
+ - [ ] Add `attach_hdr_metadata()` function
+ - [ ] Call it in `draw_frame()` after getting pixbuf
+ - [ ] Add necessary includes if needed
+- [ ] Rebuild MPVKit
+- [ ] Test with HDR10 content on tvOS
+- [ ] Verify TV shows HDR indicator
+- [ ] Test with HLG content
+- [ ] Test with Dolby Vision content (may need additional work)
+
+---
+
+## Current Implementation Status
+
+**What's implemented in Streamyfin:**
+
+1. **HDR Detection** (`MPVLayerRenderer.swift`)
+ - Reads `video-params/primaries` and `video-params/gamma` from mpv
+ - Detects HDR10 (bt.2020 + pq), HLG, Dolby Vision
+
+2. **AVDisplayCriteria** (`MpvPlayerView.swift`)
+ - Sets `preferredDisplayCriteria` on tvOS 17.0+ when HDR detected
+ - Creates CMFormatDescription with HDR color extensions
+
+3. **target-colorspace-hint** (`MPVLayerRenderer.swift`)
+ - Added `target-colorspace-hint=yes` for tvOS
+
+**What's NOT working:**
+- TV doesn't show HDR indicator
+- Colors appear washed out
+- The pixel buffers lack HDR metadata attachments ← **This is what the fix addresses**
+
+---
+
+## Industry Context
+
+| Project | Player | HDR Status |
+|---------|--------|------------|
+| [Swiftfin](https://github.com/jellyfin/Swiftfin/issues/811) | VLCKit | Washed out, uses AVPlayer for HDR |
+| [Plex](https://freetime.mikeconnelly.com/archives/8360) | mpv | No HDR support |
+| Infuse | Custom Metal engine | Works correctly |
+
+**Key insight:** No mpv-based player has solved HDR on tvOS. This fix could be a first.
+
+---
+
+## Technical References
+
+### Apple Documentation
+- [AVDisplayManager](https://developer.apple.com/documentation/avkit/avdisplaymanager)
+- [AVDisplayCriteria](https://developer.apple.com/documentation/avkit/avdisplaycriteria)
+- [WWDC22: Display HDR video in EDR](https://developer.apple.com/videos/play/wwdc2022/110565/)
+
+### CVImageBuffer Keys
+- `kCVImageBufferColorPrimariesKey` - Color gamut (BT.709, BT.2020, P3)
+- `kCVImageBufferTransferFunctionKey` - Transfer function (sRGB, PQ, HLG)
+- `kCVImageBufferYCbCrMatrixKey` - YCbCr conversion matrix
+- `kCVImageBufferMasteringDisplayColorVolumeKey` - Mastering display metadata
+- `kCVImageBufferContentLightLevelInfoKey` - MaxCLL/MaxFALL
+
+### mpv/libplacebo Source
+- mp_image struct: `video/mp_image.h`
+- Colorspace enums: libplacebo `pl_color.h`
+- vo_avfoundation: `MPVKit/Sources/BuildScripts/patch/libmpv/0004-avfoundation-video-output.patch`
+
+### Key Functions in vo_avfoundation
+| Function | Line | Purpose |
+|----------|------|---------|
+| `draw_frame()` | 781 | Main frame rendering |
+| `copy_hdr_metadata()` | 253 | Copy HDR metadata between buffers |
+| `upload_software_frame()` | 295 | Upload SW frames to CVPixelBuffer |
+| `composite_frame()` | 582 | OSD compositing with HDR support |
diff --git a/docs/tv-discovery.md b/docs/tv-discovery.md
new file mode 100644
index 000000000..b1a551656
--- /dev/null
+++ b/docs/tv-discovery.md
@@ -0,0 +1,136 @@
+# TV Discovery
+
+This document explains Streamyfin's platform-specific home screen discovery integrations for Apple TV and Android TV.
+
+## Overview
+
+Streamyfin currently publishes the same "Continue and Next Up" content to two different platform surfaces:
+
+- `tvOS`: Apple TV Top Shelf
+- `Android TV`: preview channel recommendations
+
+Both integrations are fed by the same shared payload builder in [utils/tvDiscovery/payload.ts](../utils/tvDiscovery/payload.ts).
+
+## Shared Data Flow
+
+The TV home screen data starts in [components/home/Home.tv.tsx](../components/home/Home.tv.tsx), where the app fetches resume and next-up items and passes them into [utils/tvDiscovery/sync.ts](../utils/tvDiscovery/sync.ts).
+
+The sync layer:
+
+- builds a normalized TV discovery payload
+- sends it to the tvOS Top Shelf cache writer on Apple TV
+- sends it to the Android TV recommendations module on Android TV
+- clears published content when server or user state changes
+
+## Apple TV Top Shelf
+
+Apple TV uses a Top Shelf extension target, not the main app process.
+
+Relevant files:
+
+- [plugins/withTVOSTopShelf.js](../plugins/withTVOSTopShelf.js)
+- [targets/StreamyfinTopShelf/TopShelfProvider.swift](../targets/StreamyfinTopShelf/TopShelfProvider.swift)
+- [modules/top-shelf-cache/ios/TopShelfCacheModule.swift](../modules/top-shelf-cache/ios/TopShelfCacheModule.swift)
+- [utils/topshelf/cache.ts](../utils/topshelf/cache.ts)
+
+How it works:
+
+- the app builds a lightweight JSON payload
+- the app stores that payload in the shared app group container
+- the tvOS Top Shelf extension reads the cached payload
+- the extension renders sections and items for Top Shelf
+
+Why the API key is stored on tvOS:
+
+- the Top Shelf extension runs outside the app process
+- it may need authenticated image access when loading poster artwork
+- the app stores the API key so the extension can build authenticated requests
+
+## Android TV Recommendations
+
+Android TV uses the TV provider APIs to publish a preview channel and preview programs.
+
+Relevant files:
+
+- [modules/tv-recommendations/android/src/main/java/expo/modules/tvrecommendations/TvRecommendationsPublisher.kt](../modules/tv-recommendations/android/src/main/java/expo/modules/tvrecommendations/TvRecommendationsPublisher.kt)
+- [modules/tv-recommendations/android/src/main/java/expo/modules/tvrecommendations/TvRecommendationsModule.kt](../modules/tv-recommendations/android/src/main/java/expo/modules/tvrecommendations/TvRecommendationsModule.kt)
+- [modules/tv-recommendations/android/src/main/java/expo/modules/tvrecommendations/TvRecommendationsReceiver.kt](../modules/tv-recommendations/android/src/main/java/expo/modules/tvrecommendations/TvRecommendationsReceiver.kt)
+- [modules/tv-recommendations/android/src/main/AndroidManifest.xml](../modules/tv-recommendations/android/src/main/AndroidManifest.xml)
+- [utils/tvDiscovery/sync.ts](../utils/tvDiscovery/sync.ts)
+
+How it works:
+
+- the app builds the shared TV discovery payload
+- the Android native module creates or updates a single preview channel
+- the module inserts or updates preview programs for each item
+- the module stores the last payload in shared preferences
+- the `INITIALIZE_PROGRAMS` receiver can replay the cached payload when requested by the system
+
+Important differences from tvOS:
+
+- Android TV does not use a separate extension target
+- Android TV content is persisted through `TvContractCompat`
+- artwork is currently published as poster URLs, not app-proxied local content
+
+## Logging
+
+### JavaScript logs
+
+Look for `TVDiscovery` in Metro or app logs.
+
+Examples:
+
+- payload prepared
+- Android sync result
+- clear operations
+
+### Native Android logs
+
+Use `adb logcat | grep TvRecommendations`
+
+Examples:
+
+- channel created or updated
+- preview programs inserted or updated
+- stale programs deleted
+- cached payload replayed
+
+## Verifying Android TV Output
+
+1. Launch the TV build and let the home screen load.
+2. Watch `adb logcat | grep TvRecommendations`.
+3. Return to the Android TV / Google TV home screen.
+4. Look for the `Continue and Next Up` row.
+5. If needed, enable the Streamyfin channel in `Customize home` or `Manage channels`.
+
+Note:
+
+- some launchers delay or hide new preview channels
+- some devices expose TV provider data per user/profile
+
+## Build Notes
+
+This feature does not currently require a fresh `prebuild` to work in the checked-in Android project.
+
+Why:
+
+- the Android integration is a local Expo module
+- its receiver is declared in the module manifest
+- Gradle merges it during normal Android TV builds
+
+Typical commands:
+
+- `bun run android:tv`
+- `bun run ios:tv`
+
+## Current Limitations
+
+- Android TV artwork may fail on authenticated Jellyfin servers because the launcher fetches poster URLs outside the app
+- Android TV currently publishes a preview channel only, not Watch Next
+- tvOS and Android TV both use the same payload source, so section selection is shared unless explicitly split later
+
+## Future Improvements
+
+- add a local image proxy or cache for Android TV artwork
+- add Watch Next support for resumable content
+- add a native debug dump method for querying TV provider state from inside the app process
diff --git a/docs/tv-focus-guide.md b/docs/tv-focus-guide.md
new file mode 100644
index 000000000..fc49a93e8
--- /dev/null
+++ b/docs/tv-focus-guide.md
@@ -0,0 +1,305 @@
+# TV Focus Guide Navigation
+
+This document explains how to use `TVFocusGuideView` to create reliable focus navigation between non-adjacent sections on Apple TV and Android TV.
+
+## Platform Differences (CRITICAL)
+
+### tvOS vs Android TV
+
+**`nextFocusUp`, `nextFocusDown`, `nextFocusLeft`, `nextFocusRight` props only work on Android TV, NOT tvOS.**
+
+This is a [known limitation](https://github.com/react-native-tvos/react-native-tvos/issues/490). These props are documented as "only for Android" in React Native.
+
+```typescript
+// ❌ Does NOT work on tvOS (Apple TV)
+
+ ...
+
+
+// ✅ Works on both tvOS and Android TV
+
+ ...
+
+```
+
+**For tvOS, always use `TVFocusGuideView` with the `destinations` prop.**
+
+## ScrollView vs FlatList for TV
+
+**Use ScrollView instead of FlatList for horizontal lists on TV when focus navigation is critical.**
+
+FlatList only renders visible items and manages its own recycling, which can interfere with focus navigation. ScrollView renders all items at once, providing more predictable focus behavior.
+
+```typescript
+// ❌ FlatList can cause focus issues on TV
+ }
+/>
+
+// ✅ ScrollView provides reliable focus navigation
+
+ {cast.map((person, index) => (
+
+ ))}
+
+```
+
+**When to use which:**
+- **ScrollView**: Small to medium lists (< 20 items) where focus navigation must be reliable
+- **FlatList**: Large lists where performance is more important than perfect focus navigation
+
+## The Problem
+
+tvOS uses a **geometric focus engine** that draws a ray in the navigation direction and finds the nearest focusable element. This works well for adjacent elements but fails when:
+
+- Sections are not geometrically aligned (e.g., left-aligned buttons above a horizontally-scrolling list)
+- Lists are long and the "nearest" element is in the middle rather than the first item
+- There's empty space between focusable sections
+
+**Symptoms:**
+- Focus lands in the middle of a list instead of the first item
+- Can't navigate down to a section at all
+- Focus jumps to unexpected elements
+
+## The Solution: TVFocusGuideView with destinations
+
+`TVFocusGuideView` is a React Native component that creates an invisible focus region. When combined with the `destinations` prop, it redirects focus to specific elements.
+
+### Basic Pattern
+
+```typescript
+import { TVFocusGuideView, View } from "react-native";
+
+// 1. Track the destination element with state (NOT useRef!)
+const [targetRef, setTargetRef] = useState(null);
+
+// 2. Place an invisible focus guide between sections
+{targetRef && (
+
+)}
+
+// 3. Pass the state setter as a callback ref to the target
+
+```
+
+### Why useState Instead of useRef?
+
+The focus guide only updates when it receives a prop change. Using `useRef` won't trigger re-renders when the ref is set, so the focus guide won't know about the destination. **Always use `useState`** to track refs for focus guides.
+
+```typescript
+// ❌ Won't work - useRef doesn't trigger re-renders
+const targetRef = useRef(null);
+
+
+// ✅ Works - useState triggers re-render when ref is set
+const [targetRef, setTargetRef] = useState(null);
+
+```
+
+## Bidirectional Navigation (CRITICAL PATTERN)
+
+When you need focus to navigate both UP and DOWN between sections, you must stack both focus guides together AND avoid `hasTVPreferredFocus` on the destination element.
+
+### The Focus Flickering Problem
+
+If you use `hasTVPreferredFocus={true}` on an element that is ALSO the destination of a focus guide, you will get **focus flickering** where focus rapidly jumps back and forth between elements.
+
+```typescript
+// ❌ CAUSES FOCUS FLICKERING - destination has hasTVPreferredFocus
+
+
+ {items.map((item, index) => (
+
+ ))}
+
+
+// ✅ CORRECT - destination does NOT have hasTVPreferredFocus
+
+
+ {items.map((item, index) => (
+
+ ))}
+
+```
+
+### Complete Bidirectional Example
+
+```typescript
+const MyScreen: React.FC = () => {
+ // Track refs for focus navigation
+ const [playButtonRef, setPlayButtonRef] = useState(null);
+ const [firstCastCardRef, setFirstCastCardRef] = useState(null);
+
+ return (
+
+ {/* Action buttons section */}
+
+
+ Play
+
+
+
+ {/* Cast section */}
+
+ Cast
+
+ {/* BOTH focus guides stacked together, above the list */}
+ {/* Downward: Play button → first cast card */}
+ {firstCastCardRef && (
+
+ )}
+ {/* Upward: cast → Play button */}
+ {playButtonRef && (
+
+ )}
+
+ {/* Use ScrollView, not FlatList, for reliable focus */}
+
+ {cast.map((person, index) => (
+
+ ))}
+
+
+
+ );
+};
+```
+
+### Key Rules for Bidirectional Navigation
+
+1. **Stack both focus guides together** - Place them adjacent to each other, above the destination list
+2. **Do NOT use `hasTVPreferredFocus` on focus guide destinations** - This causes focus flickering
+3. **Use ScrollView instead of FlatList** - More reliable focus behavior
+4. **Use `useState` for refs, not `useRef`** - Triggers re-renders when refs are set
+
+## Focus Guide Placement
+
+The focus guides should be placed **together** above the destination section:
+
+```
+┌─────────────────────────┐
+│ Action Buttons │ ← Source (going down)
+│ [Play] [Request] │ Has hasTVPreferredFocus ✓
+└─────────────────────────┘
+ ↓
+┌─────────────────────────┐
+│ TVFocusGuideView │ ← Downward guide
+│ destinations=[card1] │
+├─────────────────────────┤
+│ TVFocusGuideView │ ← Upward guide
+│ destinations=[playBtn] │ (stacked together)
+└─────────────────────────┘
+ ↓
+┌─────────────────────────┐
+│ Cast Cards (ScrollView)│ ← First card is destination
+│ [👤] [👤] [👤] [👤] │ NO hasTVPreferredFocus ✗
+└─────────────────────────┘
+```
+
+## Component Pattern with refSetter
+
+For components that need to be focus guide destinations, use a `refSetter` callback prop:
+
+```typescript
+interface TVCastCardProps {
+ person: { id: number; name: string };
+ onPress: () => void;
+ refSetter?: (ref: View | null) => void;
+}
+
+const TVCastCard: React.FC = ({
+ person,
+ onPress,
+ refSetter,
+}) => {
+ return (
+
+ {person.name}
+
+ );
+};
+
+// Usage
+
+```
+
+## Tips and Gotchas
+
+1. **Guard against null refs**: Only render the focus guide when the ref is set:
+ ```typescript
+ {targetRef && }
+ ```
+
+2. **Style the guide invisibly**: Use `height: 1` or `width: 1` to make it invisible but still functional:
+ ```typescript
+ style={{ height: 1, width: "100%" }}
+ ```
+
+3. **Multiple destinations**: You can provide multiple destinations and the focus engine will pick the geometrically closest one:
+ ```typescript
+
+ ```
+
+4. **Focus trapping**: Use `trapFocusUp`, `trapFocusDown`, etc. to prevent focus from leaving a region (useful for modals):
+ ```typescript
+
+ {/* Modal content */}
+
+ ```
+
+5. **Auto focus**: Use `autoFocus` to automatically focus the first focusable child when entering a region:
+ ```typescript
+
+ {/* First focusable child will receive focus */}
+
+ ```
+
+ **Warning**: Don't use `autoFocus` on a wrapper when you also have bidirectional focus guides - it can interfere with upward navigation.
+
+## Common Mistakes
+
+| Mistake | Result | Fix |
+|---------|--------|-----|
+| Using `nextFocusUp`/`nextFocusDown` props | Doesn't work on tvOS | Use `TVFocusGuideView` |
+| Using FlatList for horizontal lists | Focus navigation unreliable | Use ScrollView |
+| `hasTVPreferredFocus` on focus guide destination | Focus flickering loop | Remove `hasTVPreferredFocus` from destination |
+| Focus guides placed separately | Focus flickering | Stack both guides together |
+| Using `useRef` for focus guide refs | Focus guide doesn't update | Use `useState` |
+
+## Reference Implementation
+
+See `components/jellyseerr/tv/TVJellyseerrPage.tsx` for a complete implementation of bidirectional focus navigation between action buttons and a cast list.
diff --git a/docs/tv-modal-guide.md b/docs/tv-modal-guide.md
new file mode 100644
index 000000000..a1b57e9bd
--- /dev/null
+++ b/docs/tv-modal-guide.md
@@ -0,0 +1,416 @@
+# TV Modal Guide
+
+This document explains how to implement modals, bottom sheets, and overlays on Apple TV and Android TV in React Native.
+
+## The Problem
+
+On TV platforms, modals have unique challenges:
+- The hardware back button must work correctly to dismiss modals
+- Focus management must be handled explicitly
+- React Native's `Modal` component breaks the TV focus chain
+- Overlay/absolute-positioned modals don't handle back button correctly
+
+## Navigation-Based Modal Pattern (Recommended)
+
+For modals that need proper back button support, use the **navigation-based modal pattern**. This leverages Expo Router's stack navigation with transparent modal presentation.
+
+### Architecture
+
+```
+┌─────────────────────────────────────┐
+│ 1. Jotai Atom (state) │
+│ Stores modal data/params │
+├─────────────────────────────────────┤
+│ 2. Hook (trigger) │
+│ Sets atom + calls router.push() │
+├─────────────────────────────────────┤
+│ 3. Page File (UI) │
+│ Reads atom, renders modal │
+│ Clears atom on unmount │
+├─────────────────────────────────────┤
+│ 4. Stack.Screen (config) │
+│ presentation: transparentModal │
+│ animation: fade │
+└─────────────────────────────────────┘
+```
+
+### Step 1: Create the Atom
+
+Create a Jotai atom to store the modal state/data:
+
+```typescript
+// utils/atoms/tvExampleModal.ts
+import { atom } from "jotai";
+
+export interface TVExampleModalData {
+ itemId: string;
+ title: string;
+ // ... other data the modal needs
+}
+
+export const tvExampleModalAtom = atom(null);
+```
+
+### Step 2: Create the Hook
+
+Create a hook that sets the atom and navigates to the modal:
+
+```typescript
+// hooks/useTVExampleModal.ts
+import { useSetAtom } from "jotai";
+import { router } from "expo-router";
+import { tvExampleModalAtom, TVExampleModalData } from "@/utils/atoms/tvExampleModal";
+
+export const useTVExampleModal = () => {
+ const setModalData = useSetAtom(tvExampleModalAtom);
+
+ const openModal = (data: TVExampleModalData) => {
+ setModalData(data);
+ router.push("/tv-example-modal");
+ };
+
+ return { openModal };
+};
+```
+
+### Step 3: Create the Modal Page
+
+Create a page file that reads the atom and renders the modal UI:
+
+```typescript
+// app/(auth)/tv-example-modal.tsx
+import { useEffect } from "react";
+import { View, Pressable, Text } from "react-native";
+import { useAtom } from "jotai";
+import { router } from "expo-router";
+import { BlurView } from "expo-blur";
+import { tvExampleModalAtom } from "@/utils/atoms/tvExampleModal";
+
+export default function TVExampleModal() {
+ const [modalData, setModalData] = useAtom(tvExampleModalAtom);
+
+ // Clear atom on unmount
+ useEffect(() => {
+ return () => {
+ setModalData(null);
+ };
+ }, [setModalData]);
+
+ // Handle case where modal is opened without data
+ if (!modalData) {
+ router.back();
+ return null;
+ }
+
+ return (
+
+ {/* Background overlay */}
+ router.back()}
+ />
+
+ {/* Modal content */}
+
+
+ {modalData.title}
+
+ {/* Modal content here */}
+
+ router.back()}
+ hasTVPreferredFocus
+ style={({ focused }) => ({
+ marginTop: 24,
+ padding: 16,
+ borderRadius: 8,
+ backgroundColor: focused ? "#fff" : "rgba(255,255,255,0.1)",
+ })}
+ >
+ {({ focused }) => (
+
+ Close
+
+ )}
+
+
+
+ );
+}
+```
+
+### Step 4: Add Stack.Screen Configuration
+
+Add the modal route to `app/_layout.tsx`:
+
+```typescript
+// In app/_layout.tsx, inside your Stack navigator
+
+```
+
+### Usage
+
+```typescript
+// In any component
+import { useTVExampleModal } from "@/hooks/useTVExampleModal";
+
+const MyComponent = () => {
+ const { openModal } = useTVExampleModal();
+
+ return (
+ openModal({ itemId: "123", title: "Example" })}
+ >
+ Open Modal
+
+ );
+};
+```
+
+### Reference Implementation
+
+See `useTVRequestModal` + `app/(auth)/tv-request-modal.tsx` for a complete working example.
+
+---
+
+## Bottom Sheet Pattern (Inline Overlays)
+
+For simpler overlays that don't need back button navigation (like option selectors), use an **inline absolute-positioned overlay**. This pattern is ideal for:
+- Dropdown selectors
+- Quick action menus
+- Option pickers
+
+### Key Principles
+
+1. **Use absolute positioning instead of Modal** - React Native's `Modal` breaks the TV focus chain
+2. **Horizontal ScrollView for options** - Natural for TV remotes (left/right D-pad)
+3. **Disable background focus** - Prevent focus flickering between overlay and background
+
+### Implementation
+
+```typescript
+import { useState } from "react";
+import { View, ScrollView, Pressable, Text } from "react-native";
+import { BlurView } from "expo-blur";
+
+const TVOptionSelector: React.FC<{
+ options: { label: string; value: string }[];
+ selectedValue: string;
+ onSelect: (value: string) => void;
+ isOpen: boolean;
+ onClose: () => void;
+}> = ({ options, selectedValue, onSelect, isOpen, onClose }) => {
+ if (!isOpen) return null;
+
+ const selectedIndex = options.findIndex(o => o.value === selectedValue);
+
+ return (
+
+
+
+ {options.map((option, index) => (
+ {
+ onSelect(option.value);
+ onClose();
+ }}
+ />
+ ))}
+
+
+
+ );
+};
+```
+
+### Option Card Component
+
+```typescript
+import { useState, useRef, useEffect } from "react";
+import { Pressable, Text, Animated } from "react-native";
+
+const TVOptionCard: React.FC<{
+ label: string;
+ isSelected: boolean;
+ hasTVPreferredFocus?: boolean;
+ onPress: () => void;
+}> = ({ label, isSelected, hasTVPreferredFocus, onPress }) => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+
+ const animateTo = (toValue: number) => {
+ Animated.spring(scale, {
+ toValue,
+ useNativeDriver: true,
+ tension: 50,
+ friction: 7,
+ }).start();
+ };
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.05);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ }}
+ hasTVPreferredFocus={hasTVPreferredFocus}
+ >
+
+
+ {label}
+
+
+
+ );
+};
+```
+
+### Reference Implementation
+
+See `TVOptionSelector` and `TVOptionCard` in `components/ItemContent.tv.tsx`.
+
+---
+
+## Focus Management for Overlays
+
+**CRITICAL**: When displaying overlays on TV, you must explicitly disable focus on all background elements. Without this, the TV focus engine will rapidly switch between overlay and background elements, causing a focus loop.
+
+### Solution
+
+Add a `disabled` prop to every focusable component and pass `disabled={isModalOpen}` when an overlay is visible:
+
+```typescript
+// 1. Track modal state
+const [openModal, setOpenModal] = useState(null);
+const isModalOpen = openModal !== null;
+
+// 2. Each focusable component accepts disabled prop
+const TVFocusableButton: React.FC<{
+ onPress: () => void;
+ disabled?: boolean;
+}> = ({ onPress, disabled }) => (
+
+ {/* content */}
+
+);
+
+// 3. Pass disabled to all background components when modal is open
+
+```
+
+### Reference Implementation
+
+See `settings.tv.tsx` for a complete example with `TVSettingsOptionButton`, `TVSettingsToggle`, `TVSettingsStepper`, etc.
+
+---
+
+## Focus Trapping
+
+For modals that should trap focus (prevent navigation outside the modal), use `TVFocusGuideView` with trap props:
+
+```typescript
+import { TVFocusGuideView } from "react-native";
+
+
+ {/* Modal content - focus cannot escape */}
+
+```
+
+**Warning**: Don't use `autoFocus` on focus guide wrappers when you also have bidirectional focus guides - it can interfere with navigation.
+
+---
+
+## Common Mistakes
+
+| Mistake | Result | Fix |
+|---------|--------|-----|
+| Using React Native `Modal` | Focus chain breaks | Use navigation-based or absolute positioning |
+| Overlay without disabling background focus | Focus flickering loop | Add `disabled` prop to all background focusables |
+| No `hasTVPreferredFocus` in modal | Focus stuck on background | Set preferred focus on first modal element |
+| Missing `presentation: "transparentModal"` | Modal not transparent | Add to Stack.Screen options |
+| Not clearing atom on unmount | Stale data on reopen | Clear in useEffect cleanup |
+
+---
+
+## When to Use Which Pattern
+
+| Scenario | Pattern |
+|----------|---------|
+| Full-screen modal with back button | Navigation-based modal |
+| Confirmation dialogs | Navigation-based modal |
+| Option selectors / dropdowns | Bottom sheet (inline) |
+| Quick action menus | Bottom sheet (inline) |
+| Complex forms | Navigation-based modal |
diff --git a/eas.json b/eas.json
index 8a9736d31..03f933895 100644
--- a/eas.json
+++ b/eas.json
@@ -43,16 +43,23 @@
"EXPO_PUBLIC_WRITE_DEBUG": "1"
}
},
+ "preview_tv": {
+ "distribution": "internal",
+ "env": {
+ "EXPO_TV": "1",
+ "EXPO_PUBLIC_WRITE_DEBUG": "1"
+ }
+ },
"production": {
"environment": "production",
- "channel": "0.52.0",
+ "channel": "0.54.0",
"android": {
"image": "latest"
}
},
"production-apk": {
"environment": "production",
- "channel": "0.52.0",
+ "channel": "0.54.0",
"android": {
"buildType": "apk",
"image": "latest"
@@ -60,7 +67,7 @@
},
"production-apk-tv": {
"environment": "production",
- "channel": "0.52.0",
+ "channel": "0.54.0",
"android": {
"buildType": "apk",
"image": "latest"
@@ -68,9 +75,20 @@
"env": {
"EXPO_TV": "1"
}
+ },
+ "production_tv": {
+ "environment": "production",
+ "channel": "0.54.0",
+ "env": {
+ "EXPO_TV": "1"
+ },
+ "ios": {
+ "credentialsSource": "local"
+ }
}
},
"submit": {
- "production": {}
+ "production": {},
+ "production_tv": {}
}
}
diff --git a/hooks/useDefaultPlaySettings.ts b/hooks/useDefaultPlaySettings.ts
index 51b0e9eed..ad292639a 100644
--- a/hooks/useDefaultPlaySettings.ts
+++ b/hooks/useDefaultPlaySettings.ts
@@ -1,16 +1,27 @@
import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useMemo } from "react";
import type { Settings } from "@/utils/atoms/settings";
-import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
+import {
+ getDefaultPlaySettings,
+ type PlaySettingsOptions,
+} from "@/utils/jellyfin/getDefaultPlaySettings";
/**
* React hook wrapper for getDefaultPlaySettings.
* Used in UI components for initial playback (no previous track state).
+ *
+ * @param item - The media item to play
+ * @param settings - User settings (language preferences, bitrate, etc.)
+ * @param options - Optional flags to control behavior (e.g., applyLanguagePreferences for TV)
*/
-const useDefaultPlaySettings = (item: BaseItemDto, settings: Settings | null) =>
+const useDefaultPlaySettings = (
+ item: BaseItemDto | null | undefined,
+ settings: Settings | null,
+ options?: PlaySettingsOptions,
+) =>
useMemo(() => {
const { mediaSource, audioIndex, subtitleIndex, bitrate } =
- getDefaultPlaySettings(item, settings);
+ getDefaultPlaySettings(item, settings, undefined, options);
return {
defaultMediaSource: mediaSource,
@@ -18,6 +29,6 @@ const useDefaultPlaySettings = (item: BaseItemDto, settings: Settings | null) =>
defaultSubtitleIndex: subtitleIndex,
defaultBitrate: bitrate,
};
- }, [item, settings]);
+ }, [item, settings, options]);
export default useDefaultPlaySettings;
diff --git a/hooks/useItemQuery.ts b/hooks/useItemQuery.ts
index 370b5f35a..d45fe51c6 100644
--- a/hooks/useItemQuery.ts
+++ b/hooks/useItemQuery.ts
@@ -2,6 +2,7 @@ import { ItemFields } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
+import { Platform } from "react-native";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
@@ -12,11 +13,17 @@ export const excludeFields = (fieldsToExclude: ItemFields[]) => {
);
};
+type ExtraQueryOptions = {
+ gcTime?: number;
+ staleTime?: number;
+};
+
export const useItemQuery = (
itemId: string | undefined,
isOffline?: boolean,
fields?: ItemFields[],
excludeFields?: ItemFields[],
+ queryOptions?: ExtraQueryOptions,
) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -49,9 +56,12 @@ export const useItemQuery = (
return response.data.Items?.[0];
},
enabled: !!itemId,
+ staleTime: isOffline ? Infinity : 60 * 1000,
+ refetchInterval: !isOffline && Platform.isTV ? 60 * 1000 : undefined,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
networkMode: "always",
+ ...queryOptions,
});
};
diff --git a/hooks/usePlaybackManager.ts b/hooks/usePlaybackManager.ts
index 0c90b73b0..97d148454 100644
--- a/hooks/usePlaybackManager.ts
+++ b/hooks/usePlaybackManager.ts
@@ -80,7 +80,7 @@ export const usePlaybackManager = ({
const { data: adjacentItems } = useQuery({
queryKey: ["adjacentItems", item?.Id, item?.SeriesId, isOffline],
queryFn: async (): Promise => {
- if (!item || !item.SeriesId) {
+ if (!item?.SeriesId) {
return null;
}
diff --git a/hooks/useRefreshLibraryOnFocus.ts b/hooks/useRefreshLibraryOnFocus.ts
new file mode 100644
index 000000000..f89ebd58c
--- /dev/null
+++ b/hooks/useRefreshLibraryOnFocus.ts
@@ -0,0 +1,50 @@
+import { useFocusEffect } from "expo-router";
+import { useCallback, useRef } from "react";
+import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
+
+// Query keys that depend on the set of library items. Kept in sync with the
+// LibraryChanged handler in WebSocketProvider.
+const LIBRARY_QUERY_KEYS = [
+ ["home"],
+ ["library-items"],
+ ["nextUp-all"],
+ ["nextUp"],
+ ["resumeItems"],
+];
+
+/**
+ * Fallback refresh for newly added/removed content.
+ *
+ * The primary path is the server's `LibraryChanged` WebSocket event (handled in
+ * WebSocketProvider). This hook is a safety net for cases where the socket was
+ * down or the change happened while the screen was unfocused: when the screen
+ * regains focus, it invalidates the library-dependent queries so React Query
+ * refetches the latest content.
+ *
+ * Skips the refresh on the very first focus (initial mount already fetches) and
+ * throttles to avoid refetch storms when quickly switching tabs.
+ */
+export function useRefreshLibraryOnFocus(throttleMs = 30_000) {
+ const queryClient = useNetworkAwareQueryClient();
+ const hasFocusedOnce = useRef(false);
+ const lastRefreshRef = useRef(0);
+
+ useFocusEffect(
+ useCallback(() => {
+ if (!hasFocusedOnce.current) {
+ hasFocusedOnce.current = true;
+ return;
+ }
+
+ const now = Date.now();
+ if (now - lastRefreshRef.current < throttleMs) {
+ return;
+ }
+ lastRefreshRef.current = now;
+
+ for (const queryKey of LIBRARY_QUERY_KEYS) {
+ queryClient.invalidateQueries({ queryKey });
+ }
+ }, [queryClient, throttleMs]),
+ );
+}
diff --git a/hooks/useRemoteSubtitles.ts b/hooks/useRemoteSubtitles.ts
new file mode 100644
index 000000000..b101aeeee
--- /dev/null
+++ b/hooks/useRemoteSubtitles.ts
@@ -0,0 +1,332 @@
+import type {
+ BaseItemDto,
+ RemoteSubtitleInfo,
+} from "@jellyfin/sdk/lib/generated-client";
+import { getSubtitleApi } from "@jellyfin/sdk/lib/utils/api";
+import { useMutation } from "@tanstack/react-query";
+import { Directory, File, Paths } from "expo-file-system";
+import { useAtomValue } from "jotai";
+import { useCallback, useMemo } from "react";
+import { Platform } from "react-native";
+import { apiAtom } from "@/providers/JellyfinProvider";
+import {
+ addDownloadedSubtitle,
+ type DownloadedSubtitle,
+} from "@/utils/atoms/downloadedSubtitles";
+import { useSettings } from "@/utils/atoms/settings";
+import {
+ OpenSubtitlesApi,
+ type OpenSubtitlesResult,
+} from "@/utils/opensubtitles/api";
+
+export interface SubtitleSearchResult {
+ id: string;
+ name: string;
+ providerName: string;
+ format: string;
+ language: string;
+ communityRating?: number;
+ downloadCount?: number;
+ isHashMatch?: boolean;
+ hearingImpaired?: boolean;
+ aiTranslated?: boolean;
+ machineTranslated?: boolean;
+ /** For OpenSubtitles: file ID to download */
+ fileId?: number;
+ /** Source: 'jellyfin' or 'opensubtitles' */
+ source: "jellyfin" | "opensubtitles";
+}
+
+interface UseRemoteSubtitlesOptions {
+ itemId: string;
+ item: BaseItemDto;
+ mediaSourceId?: string | null;
+}
+
+/**
+ * Convert Jellyfin RemoteSubtitleInfo to unified SubtitleSearchResult
+ */
+function jellyfinToResult(sub: RemoteSubtitleInfo): SubtitleSearchResult {
+ return {
+ id: sub.Id ?? "",
+ name: sub.Name ?? "Unknown",
+ providerName: sub.ProviderName ?? "Unknown",
+ format: sub.Format ?? "srt",
+ language: sub.ThreeLetterISOLanguageName ?? "",
+ communityRating: sub.CommunityRating ?? undefined,
+ downloadCount: sub.DownloadCount ?? undefined,
+ isHashMatch: sub.IsHashMatch ?? undefined,
+ hearingImpaired: sub.HearingImpaired ?? undefined,
+ aiTranslated: sub.AiTranslated ?? undefined,
+ machineTranslated: sub.MachineTranslated ?? undefined,
+ source: "jellyfin",
+ };
+}
+
+/**
+ * Convert OpenSubtitles result to unified SubtitleSearchResult
+ */
+function openSubtitlesToResult(
+ sub: OpenSubtitlesResult,
+): SubtitleSearchResult | null {
+ const firstFile = sub.attributes.files[0];
+ if (!firstFile) return null;
+
+ return {
+ id: sub.id,
+ name:
+ sub.attributes.release || sub.attributes.files[0]?.file_name || "Unknown",
+ providerName: "OpenSubtitles",
+ format: sub.attributes.format || "srt",
+ language: sub.attributes.language,
+ communityRating: sub.attributes.ratings,
+ downloadCount: sub.attributes.download_count,
+ isHashMatch: false,
+ hearingImpaired: sub.attributes.hearing_impaired,
+ aiTranslated: sub.attributes.ai_translated,
+ machineTranslated: sub.attributes.machine_translated,
+ fileId: firstFile.file_id,
+ source: "opensubtitles",
+ };
+}
+
+/**
+ * Hook for searching and downloading remote subtitles
+ *
+ * Primary: Uses Jellyfin's subtitle API (server-side OpenSubtitles plugin)
+ * Fallback: Direct OpenSubtitles API when server has no provider
+ */
+export function useRemoteSubtitles({
+ itemId,
+ item,
+ mediaSourceId: _mediaSourceId,
+}: UseRemoteSubtitlesOptions) {
+ const api = useAtomValue(apiAtom);
+ const { settings } = useSettings();
+ const openSubtitlesApiKey = settings.openSubtitlesApiKey;
+
+ // Check if we can use OpenSubtitles fallback
+ const hasOpenSubtitlesApiKey = Boolean(openSubtitlesApiKey);
+
+ // Create OpenSubtitles API client when API key is available
+ const openSubtitlesApi = useMemo(() => {
+ if (!openSubtitlesApiKey) return null;
+ return new OpenSubtitlesApi(openSubtitlesApiKey);
+ }, [openSubtitlesApiKey]);
+
+ /**
+ * Search for subtitles via Jellyfin API
+ */
+ const searchJellyfin = useCallback(
+ async (language: string): Promise => {
+ if (!api) throw new Error("API not available");
+
+ const subtitleApi = getSubtitleApi(api);
+ const response = await subtitleApi.searchRemoteSubtitles({
+ itemId,
+ language,
+ });
+
+ return (response.data || []).map(jellyfinToResult);
+ },
+ [api, itemId],
+ );
+
+ /**
+ * Search for subtitles via OpenSubtitles direct API
+ */
+ const searchOpenSubtitles = useCallback(
+ async (language: string): Promise => {
+ if (!openSubtitlesApi) {
+ throw new Error("OpenSubtitles API key not configured");
+ }
+
+ // Get IMDB ID from item if available
+ const imdbId = item.ProviderIds?.Imdb;
+
+ // Build search params
+ const params: Parameters[0] = {
+ languages: language,
+ };
+
+ if (imdbId) {
+ params.imdbId = imdbId;
+ } else {
+ // Fall back to title search
+ params.query = item.Name || "";
+ params.year = item.ProductionYear || undefined;
+ }
+
+ // For TV episodes, add season/episode info
+ if (item.Type === "Episode") {
+ params.seasonNumber = item.ParentIndexNumber || undefined;
+ params.episodeNumber = item.IndexNumber || undefined;
+ }
+
+ const response = await openSubtitlesApi.search(params);
+
+ return response.data
+ .map(openSubtitlesToResult)
+ .filter((r): r is SubtitleSearchResult => r !== null);
+ },
+ [openSubtitlesApi, item],
+ );
+
+ /**
+ * Download subtitle via Jellyfin API (saves to server library)
+ */
+ const downloadJellyfin = useCallback(
+ async (subtitleId: string): Promise => {
+ if (!api) throw new Error("API not available");
+
+ const subtitleApi = getSubtitleApi(api);
+ await subtitleApi.downloadRemoteSubtitles({
+ itemId,
+ subtitleId,
+ });
+ },
+ [api, itemId],
+ );
+
+ /**
+ * Download subtitle via OpenSubtitles API (returns local file path)
+ *
+ * On TV: Downloads to cache directory and persists metadata in MMKV
+ * On mobile: Downloads to cache directory (ephemeral, no persistence)
+ *
+ * Uses a flat filename structure with itemId prefix to avoid tvOS permission issues
+ */
+ const downloadOpenSubtitles = useCallback(
+ async (
+ fileId: number,
+ result: SubtitleSearchResult,
+ ): Promise<{ path: string; subtitle?: DownloadedSubtitle }> => {
+ if (!openSubtitlesApi) {
+ throw new Error("OpenSubtitles API key not configured");
+ }
+
+ // Get download link
+ const response = await openSubtitlesApi.download(fileId);
+ const originalFileName = response.file_name || `subtitle_${fileId}.srt`;
+
+ // Use cache directory for both platforms (tvOS has permission issues with documents)
+ // TV: Uses itemId prefix for organization and persists metadata
+ // Mobile: Simple filename, no persistence
+ const subtitlesDir = new Directory(Paths.cache, "streamyfin-subtitles");
+
+ // Ensure directory exists
+ if (!subtitlesDir.exists) {
+ subtitlesDir.create();
+ }
+
+ // TV: Prefix filename with itemId for organization
+ // Mobile: Use original filename
+ const fileName = Platform.isTV
+ ? `${itemId}_${originalFileName}`
+ : originalFileName;
+
+ // Create file and download
+ const destination = new File(subtitlesDir, fileName);
+
+ // Delete existing file if it exists (re-download)
+ if (destination.exists) {
+ destination.delete();
+ }
+
+ await File.downloadFileAsync(response.link, destination);
+
+ // TV: Persist metadata for future sessions
+ if (Platform.isTV) {
+ const subtitleMetadata: DownloadedSubtitle = {
+ id: result.id,
+ itemId,
+ filePath: destination.uri,
+ name: result.name,
+ language: result.language,
+ format: result.format,
+ source: "opensubtitles",
+ downloadedAt: Date.now(),
+ };
+ addDownloadedSubtitle(subtitleMetadata);
+ return { path: destination.uri, subtitle: subtitleMetadata };
+ }
+
+ return { path: destination.uri };
+ },
+ [openSubtitlesApi, itemId],
+ );
+
+ /**
+ * Search mutation - tries Jellyfin first, falls back to OpenSubtitles
+ */
+ const searchMutation = useMutation({
+ mutationFn: async ({
+ language,
+ preferOpenSubtitles = false,
+ }: {
+ language: string;
+ preferOpenSubtitles?: boolean;
+ }) => {
+ // If user prefers OpenSubtitles and has API key, use it
+ if (preferOpenSubtitles && hasOpenSubtitlesApiKey) {
+ return searchOpenSubtitles(language);
+ }
+
+ // Try Jellyfin first
+ try {
+ const results = await searchJellyfin(language);
+ // If no results and we have OpenSubtitles fallback, try it
+ if (results.length === 0 && hasOpenSubtitlesApiKey) {
+ return searchOpenSubtitles(language);
+ }
+ return results;
+ } catch (error) {
+ // If Jellyfin fails (no provider configured) and we have fallback, use it
+ if (hasOpenSubtitlesApiKey) {
+ return searchOpenSubtitles(language);
+ }
+ throw error;
+ }
+ },
+ });
+
+ /**
+ * Download mutation
+ */
+ const downloadMutation = useMutation({
+ mutationFn: async (result: SubtitleSearchResult) => {
+ if (result.source === "jellyfin") {
+ await downloadJellyfin(result.id);
+ return { type: "server" as const };
+ }
+ if (result.fileId) {
+ const { path, subtitle } = await downloadOpenSubtitles(
+ result.fileId,
+ result,
+ );
+ return { type: "local" as const, path, subtitle };
+ }
+ throw new Error("Invalid subtitle result");
+ },
+ });
+
+ return {
+ // State
+ hasOpenSubtitlesApiKey,
+ isSearching: searchMutation.isPending,
+ isDownloading: downloadMutation.isPending,
+ searchError: searchMutation.error,
+ downloadError: downloadMutation.error,
+ searchResults: searchMutation.data,
+
+ // Actions
+ search: searchMutation.mutate,
+ searchAsync: searchMutation.mutateAsync,
+ download: downloadMutation.mutate,
+ downloadAsync: downloadMutation.mutateAsync,
+ reset: () => {
+ searchMutation.reset();
+ downloadMutation.reset();
+ },
+ };
+}
diff --git a/hooks/useSessions.ts b/hooks/useSessions.ts
index 5aba65159..108441c0e 100644
--- a/hooks/useSessions.ts
+++ b/hooks/useSessions.ts
@@ -21,7 +21,7 @@ export const useSessions = ({
const { data, isLoading } = useQuery({
queryKey: ["sessions"],
queryFn: async () => {
- if (!api || !user || !user.Policy?.IsAdministrator) {
+ if (!api || !user?.Policy?.IsAdministrator) {
return [];
}
const response = await getSessionApi(api).getSessions({
@@ -55,7 +55,7 @@ export const useAllSessions = ({
const { data, isLoading } = useQuery({
queryKey: ["allSessions"],
queryFn: async () => {
- if (!api || !user || !user.Policy?.IsAdministrator) {
+ if (!api || !user?.Policy?.IsAdministrator) {
return [];
}
const response = await getSessionApi(api).getSessions({
diff --git a/hooks/useTVAccountActionModal.ts b/hooks/useTVAccountActionModal.ts
new file mode 100644
index 000000000..97db7ac50
--- /dev/null
+++ b/hooks/useTVAccountActionModal.ts
@@ -0,0 +1,34 @@
+import { useCallback } from "react";
+import useRouter from "@/hooks/useAppRouter";
+import { tvAccountActionModalAtom } from "@/utils/atoms/tvAccountActionModal";
+import type {
+ SavedServer,
+ SavedServerAccount,
+} from "@/utils/secureCredentials";
+import { store } from "@/utils/store";
+
+interface ShowAccountActionModalParams {
+ server: SavedServer;
+ account: SavedServerAccount;
+ onLogin: () => void;
+ onDelete: () => void;
+}
+
+export const useTVAccountActionModal = () => {
+ const router = useRouter();
+
+ const showAccountActionModal = useCallback(
+ (params: ShowAccountActionModalParams) => {
+ store.set(tvAccountActionModalAtom, {
+ server: params.server,
+ account: params.account,
+ onLogin: params.onLogin,
+ onDelete: params.onDelete,
+ });
+ router.push("/tv-account-action-modal");
+ },
+ [router],
+ );
+
+ return { showAccountActionModal };
+};
diff --git a/hooks/useTVAccountSelectModal.ts b/hooks/useTVAccountSelectModal.ts
new file mode 100644
index 000000000..3bc61ed77
--- /dev/null
+++ b/hooks/useTVAccountSelectModal.ts
@@ -0,0 +1,34 @@
+import { useCallback } from "react";
+import useRouter from "@/hooks/useAppRouter";
+import { tvAccountSelectModalAtom } from "@/utils/atoms/tvAccountSelectModal";
+import type {
+ SavedServer,
+ SavedServerAccount,
+} from "@/utils/secureCredentials";
+import { store } from "@/utils/store";
+
+interface ShowAccountSelectModalParams {
+ server: SavedServer;
+ onAccountAction: (account: SavedServerAccount) => void;
+ onAddAccount: () => void;
+ onDeleteServer: () => void;
+}
+
+export const useTVAccountSelectModal = () => {
+ const router = useRouter();
+
+ const showAccountSelectModal = useCallback(
+ (params: ShowAccountSelectModalParams) => {
+ store.set(tvAccountSelectModalAtom, {
+ server: params.server,
+ onAccountAction: params.onAccountAction,
+ onAddAccount: params.onAddAccount,
+ onDeleteServer: params.onDeleteServer,
+ });
+ router.push("/tv-account-select-modal");
+ },
+ [router],
+ );
+
+ return { showAccountSelectModal };
+};
diff --git a/hooks/useTVBackHandler.ts b/hooks/useTVBackHandler.ts
new file mode 100644
index 000000000..8277d0a79
--- /dev/null
+++ b/hooks/useTVBackHandler.ts
@@ -0,0 +1,67 @@
+import { useSegments } from "expo-router";
+import { useEffect } from "react";
+import { Platform } from "react-native";
+import {
+ disableTVMenuKeyInterception,
+ enableTVMenuKeyInterception,
+} from "./useTVBackPress";
+
+export { enableTVMenuKeyInterception } from "./useTVBackPress";
+
+/**
+ * Check if we're at the root of a tab
+ */
+function isAtTabRoot(segments: string[]): boolean {
+ const lastSegment = segments[segments.length - 1];
+ const tabNames = [
+ "(home)",
+ "(search)",
+ "(favorites)",
+ "(libraries)",
+ "(watchlists)",
+ "(settings)",
+ "(custom-links)",
+ ];
+ return tabNames.includes(lastSegment) || lastSegment === "index";
+}
+
+/**
+ * Get the current tab name from segments
+ */
+function getCurrentTab(segments: string[]): string | undefined {
+ return segments.find(
+ (s) =>
+ s === "(home)" ||
+ s === "(search)" ||
+ s === "(favorites)" ||
+ s === "(libraries)" ||
+ s === "(watchlists)" ||
+ s === "(settings)" ||
+ s === "(custom-links)",
+ );
+}
+
+/**
+ * Keeps tvOS menu key interception disabled on the home tab root so the system
+ * can apply its native app-exit behavior. Other routes can opt into
+ * interception when they need JS-owned back handling.
+ */
+export function useTVHomeBackHandler() {
+ const segments = useSegments();
+
+ // Get current state
+ const currentTab = getCurrentTab(segments);
+ const atTabRoot = isAtTabRoot(segments);
+ const isOnHomeRoot = atTabRoot && currentTab === "(home)";
+
+ useEffect(() => {
+ if (!Platform.isTV) return;
+
+ if (isOnHomeRoot) {
+ disableTVMenuKeyInterception();
+ return;
+ }
+
+ enableTVMenuKeyInterception();
+ }, [isOnHomeRoot]);
+}
diff --git a/hooks/useTVBackPress.ts b/hooks/useTVBackPress.ts
new file mode 100644
index 000000000..2631cdab5
--- /dev/null
+++ b/hooks/useTVBackPress.ts
@@ -0,0 +1,72 @@
+import { type DependencyList, useEffect } from "react";
+import { BackHandler, Platform } from "react-native";
+
+type TVBackPressHandler = () => boolean | null | undefined;
+
+let TVEventControl: {
+ enableTVMenuKey: () => void;
+ disableTVMenuKey: () => void;
+} | null = null;
+
+if (Platform.isTV) {
+ try {
+ TVEventControl = require("react-native").TVEventControl;
+ } catch {
+ TVEventControl = null;
+ }
+}
+
+export function enableTVMenuKeyInterception() {
+ if (Platform.isTV && TVEventControl) {
+ TVEventControl.enableTVMenuKey();
+ }
+}
+
+export function disableTVMenuKeyInterception() {
+ if (Platform.isTV && TVEventControl) {
+ TVEventControl.disableTVMenuKey();
+ }
+}
+
+export function useTVMenuKeyInterception(enabled = true) {
+ useEffect(() => {
+ if (!Platform.isTV) return;
+
+ if (enabled) {
+ enableTVMenuKeyInterception();
+ return;
+ }
+
+ disableTVMenuKeyInterception();
+ }, [enabled]);
+}
+
+/**
+ * Subscribe to TV back presses through React Native's BackHandler.
+ *
+ * On Android TV this handles the hardware back button. On tvOS,
+ * react-native-tvos maps the Apple TV menu button to the same API when menu key
+ * interception is enabled.
+ *
+ * @see https://reactnative.dev/docs/backhandler
+ */
+export function useTVBackPress(
+ handler: TVBackPressHandler,
+ deps: DependencyList,
+) {
+ useEffect(() => {
+ if (!Platform.isTV) return;
+
+ // BackHandler is the shared back/menu surface for TV platforms:
+ // Android TV sends hardware back here, and react-native-tvos sends menu
+ // here when menu key interception is enabled.
+ const subscription = BackHandler.addEventListener(
+ "hardwareBackPress",
+ handler,
+ );
+
+ return () => {
+ subscription.remove();
+ };
+ }, deps);
+}
diff --git a/hooks/useTVEventHandler.ts b/hooks/useTVEventHandler.ts
new file mode 100644
index 000000000..d92011b75
--- /dev/null
+++ b/hooks/useTVEventHandler.ts
@@ -0,0 +1,17 @@
+import type { HWEvent } from "react-native";
+import { Platform } from "react-native";
+
+type UseTVEventHandler = (callback: (evt: HWEvent) => void) => void;
+
+let tvEventHandler: UseTVEventHandler = () => {};
+
+if (Platform.isTV) {
+ try {
+ tvEventHandler = require("react-native")
+ .useTVEventHandler as UseTVEventHandler;
+ } catch {
+ tvEventHandler = () => {};
+ }
+}
+
+export const useTVEventHandler = tvEventHandler;
diff --git a/hooks/useTVItemActionModal.ts b/hooks/useTVItemActionModal.ts
new file mode 100644
index 000000000..3c547c0d6
--- /dev/null
+++ b/hooks/useTVItemActionModal.ts
@@ -0,0 +1,82 @@
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { useQueryClient } from "@tanstack/react-query";
+import { useCallback } from "react";
+import { useTranslation } from "react-i18next";
+import { Alert } from "react-native";
+import { usePlaybackManager } from "@/hooks/usePlaybackManager";
+import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
+
+export const useTVItemActionModal = () => {
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+ const { markItemPlayed, markItemUnplayed } = usePlaybackManager();
+ const invalidatePlaybackProgressCache = useInvalidatePlaybackProgressCache();
+
+ const showItemActions = useCallback(
+ (item: BaseItemDto) => {
+ const isPlayed = item.UserData?.Played ?? false;
+ const itemTitle =
+ item.Type === "Episode"
+ ? `${item.SeriesName} - ${item.Name}`
+ : (item.Name ?? "");
+
+ const actionLabel = isPlayed
+ ? t("item_card.mark_unplayed")
+ : t("item_card.mark_played");
+
+ Alert.alert(itemTitle, undefined, [
+ { text: t("common.cancel"), style: "cancel" },
+ {
+ text: actionLabel,
+ onPress: async () => {
+ if (!item.Id) return;
+
+ // Optimistic update
+ queryClient.setQueriesData(
+ { queryKey: ["item", item.Id] },
+ (old) => {
+ if (!old) return old;
+ return {
+ ...old,
+ UserData: {
+ ...old.UserData,
+ Played: !isPlayed,
+ PlaybackPositionTicks: 0,
+ PlayedPercentage: 0,
+ },
+ };
+ },
+ );
+
+ try {
+ if (!isPlayed) {
+ await markItemPlayed(item.Id);
+ } else {
+ await markItemUnplayed(item.Id);
+ }
+ } catch {
+ // Revert on failure
+ queryClient.invalidateQueries({
+ queryKey: ["item", item.Id],
+ });
+ } finally {
+ await invalidatePlaybackProgressCache();
+ queryClient.invalidateQueries({
+ queryKey: ["item", item.Id],
+ });
+ }
+ },
+ },
+ ]);
+ },
+ [
+ t,
+ queryClient,
+ markItemPlayed,
+ markItemUnplayed,
+ invalidatePlaybackProgressCache,
+ ],
+ );
+
+ return { showItemActions };
+};
diff --git a/hooks/useTVOptionModal.ts b/hooks/useTVOptionModal.ts
new file mode 100644
index 000000000..c6acffe83
--- /dev/null
+++ b/hooks/useTVOptionModal.ts
@@ -0,0 +1,36 @@
+import { useCallback } from "react";
+import useRouter from "@/hooks/useAppRouter";
+import {
+ type TVOptionItem,
+ tvOptionModalAtom,
+} from "@/utils/atoms/tvOptionModal";
+import { store } from "@/utils/store";
+
+interface ShowOptionsParams {
+ title: string;
+ options: TVOptionItem[];
+ onSelect: (value: T) => void;
+ cardWidth?: number;
+ cardHeight?: number;
+}
+
+export const useTVOptionModal = () => {
+ const router = useRouter();
+
+ const showOptions = useCallback(
+ (params: ShowOptionsParams) => {
+ // Use store.set for synchronous update before navigation
+ store.set(tvOptionModalAtom, {
+ title: params.title,
+ options: params.options,
+ onSelect: params.onSelect,
+ cardWidth: params.cardWidth,
+ cardHeight: params.cardHeight,
+ });
+ router.push("/(auth)/tv-option-modal");
+ },
+ [router],
+ );
+
+ return { showOptions };
+};
diff --git a/hooks/useTVRequestModal.ts b/hooks/useTVRequestModal.ts
new file mode 100644
index 000000000..0c096bb46
--- /dev/null
+++ b/hooks/useTVRequestModal.ts
@@ -0,0 +1,34 @@
+import { useCallback } from "react";
+import useRouter from "@/hooks/useAppRouter";
+import { tvRequestModalAtom } from "@/utils/atoms/tvRequestModal";
+import type { MediaType } from "@/utils/jellyseerr/server/constants/media";
+import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
+import { store } from "@/utils/store";
+
+interface ShowRequestModalParams {
+ requestBody: MediaRequestBody;
+ title: string;
+ id: number;
+ mediaType: MediaType;
+ onRequested: () => void;
+}
+
+export const useTVRequestModal = () => {
+ const router = useRouter();
+
+ const showRequestModal = useCallback(
+ (params: ShowRequestModalParams) => {
+ store.set(tvRequestModalAtom, {
+ requestBody: params.requestBody,
+ title: params.title,
+ id: params.id,
+ mediaType: params.mediaType,
+ onRequested: params.onRequested,
+ });
+ router.push("/(auth)/tv-request-modal");
+ },
+ [router],
+ );
+
+ return { showRequestModal };
+};
diff --git a/hooks/useTVSeasonSelectModal.ts b/hooks/useTVSeasonSelectModal.ts
new file mode 100644
index 000000000..7b2f4f201
--- /dev/null
+++ b/hooks/useTVSeasonSelectModal.ts
@@ -0,0 +1,23 @@
+import { useCallback } from "react";
+import useRouter from "@/hooks/useAppRouter";
+import {
+ type TVSeasonSelectModalState,
+ tvSeasonSelectModalAtom,
+} from "@/utils/atoms/tvSeasonSelectModal";
+import { store } from "@/utils/store";
+
+type ShowSeasonSelectModalParams = NonNullable;
+
+export const useTVSeasonSelectModal = () => {
+ const router = useRouter();
+
+ const showSeasonSelectModal = useCallback(
+ (params: ShowSeasonSelectModalParams) => {
+ store.set(tvSeasonSelectModalAtom, params);
+ router.push("/(auth)/tv-season-select-modal");
+ },
+ [router],
+ );
+
+ return { showSeasonSelectModal };
+};
diff --git a/hooks/useTVSeriesSeasonModal.ts b/hooks/useTVSeriesSeasonModal.ts
new file mode 100644
index 000000000..dcd5d4784
--- /dev/null
+++ b/hooks/useTVSeriesSeasonModal.ts
@@ -0,0 +1,34 @@
+import { useCallback } from "react";
+import useRouter from "@/hooks/useAppRouter";
+import { tvSeriesSeasonModalAtom } from "@/utils/atoms/tvSeriesSeasonModal";
+import { store } from "@/utils/store";
+
+interface ShowSeasonModalParams {
+ seasons: Array<{
+ label: string;
+ value: number;
+ selected: boolean;
+ }>;
+ selectedSeasonIndex: number | string;
+ itemId: string;
+ onSeasonSelect: (seasonIndex: number) => void;
+}
+
+export const useTVSeriesSeasonModal = () => {
+ const router = useRouter();
+
+ const showSeasonModal = useCallback(
+ (params: ShowSeasonModalParams) => {
+ store.set(tvSeriesSeasonModalAtom, {
+ seasons: params.seasons,
+ selectedSeasonIndex: params.selectedSeasonIndex,
+ itemId: params.itemId,
+ onSeasonSelect: params.onSeasonSelect,
+ });
+ router.push("/(auth)/tv-series-season-modal");
+ },
+ [router],
+ );
+
+ return { showSeasonModal };
+};
diff --git a/hooks/useTVSubtitleModal.ts b/hooks/useTVSubtitleModal.ts
new file mode 100644
index 000000000..38d442239
--- /dev/null
+++ b/hooks/useTVSubtitleModal.ts
@@ -0,0 +1,40 @@
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
+import { useCallback } from "react";
+import type { Track } from "@/components/video-player/controls/types";
+import useRouter from "@/hooks/useAppRouter";
+import { tvSubtitleModalAtom } from "@/utils/atoms/tvSubtitleModal";
+import { store } from "@/utils/store";
+
+interface ShowSubtitleModalParams {
+ item: BaseItemDto;
+ mediaSourceId?: string | null;
+ subtitleTracks: Track[];
+ currentSubtitleIndex: number;
+ onDisableSubtitles?: () => void;
+ onServerSubtitleDownloaded?: () => void;
+ onLocalSubtitleDownloaded?: (path: string) => void;
+ refreshSubtitleTracks?: () => Promise