Compare commits

...

33 Commits

Author SHA1 Message Date
Gauvain
8e91eba5c4 Merge branch 'develop' into view-password 2025-09-04 00:19:17 +02:00
Fredrik Burmester
0b9bbb63eb fix: download card design and percentage negative number fix
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
2025-09-03 22:26:57 +02:00
Fredrik Burmester
b4014c922e feat: native download notifications (#1006) 2025-09-03 21:50:25 +02:00
stenlan
957e60714a fix: BaseItemDto/BaseItemPerson routing bug (#1030) 2025-09-03 21:45:15 +02:00
stenlan
93a63f6b48 fix: #1007 Season episodes temporarily disappearing when marking one as watched (#1031)
Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
2025-09-03 21:42:03 +02:00
stenlan
c778956a52 fix: Jellyseerr discovery crash (#1032) 2025-09-03 21:41:35 +02:00
Gauvain
d563577c8f Merge branch 'develop' into view-password 2025-09-03 15:47:18 +02:00
MarcoCoreDuo
6308375438 fix: remove unnecessary sort for getTvShowsApi.getEpisodes data in SeasonPicker (#984)
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com>
2025-09-03 15:32:57 +02:00
lance chant
2d3344f013 fix: home screen translation for custom headers (#1001)
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com>
2025-09-03 15:31:43 +02:00
Fredrik Burmester
ae720d6bb4 fix: height for list items 2025-09-03 15:14:36 +02:00
lance chant
62b25d7bf7 fix: offline checking (#989) 2025-09-03 12:26:56 +02:00
Copilot
68e3b74e49 Fix Jellyseerr TV request permission logic bug (#1026)
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
2025-09-03 07:05:46 +02:00
Gauvain
5676359138 Merge branch 'develop' into view-password 2025-09-02 15:34:47 +02:00
Uruk
e8c9bb1730 Revert "feat: add password visibility toggle to login forms (#1019)"
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
This reverts commit ae09a59569.
2025-09-02 15:33:41 +02:00
Gauvain
ae09a59569 feat: add password visibility toggle to login forms (#1019)
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
2025-09-01 23:45:12 +02:00
Uruk
6ea93ef8f0 fix: correct wording for password visibility toggle in translations 2025-09-01 23:40:46 +02:00
Uruk
d5aa812e04 chore: replace postinstall-postinstall with postinstall package
Updates dependency from postinstall-postinstall v2.1.0 to postinstall v0.11.2

Also includes dependency version updates for glob, minimatch, jackspeak, and path-scurry packages with their associated nested dependencies

Adds @expo/vector-icons to trusted dependencies list for improved build reliability
2025-09-01 23:40:01 +02:00
Uruk
79a785a615 chore: update sonner-native to v0.21.1
Incorporates latest bug fixes and improvements from the sonner-native library to enhance toast notification functionality and stability.
2025-09-01 23:34:11 +02:00
Uruk
4b2fcee460 feat: add accessibility support for password toggle
Enhances password input component with proper accessibility attributes including role, label, hint, and state to improve screen reader experience.

Adjusts minimum height handling for Android platform compatibility in list items and replaces Tailwind classes with inline styles for gesture control settings to ensure consistent styling across platforms.

Adds translation keys for password visibility toggle functionality.
2025-09-01 23:19:12 +02:00
Uruk
a93b935df3 style: improve login form spacing and alignment
Adjusts margin classes and offsets to create better visual hierarchy and spacing between form elements.

Adds bottom margins to input fields and containers, increases top offset for password input, and adds top margins to button containers for improved layout consistency.
2025-09-01 22:57:17 +02:00
Uruk
29d3360a10 refactor: standardize Input component prop naming
Replaces `className` prop with `extraClassName` across Input and PasswordInput components for consistency.

Updates PasswordInput to use numeric `topOffset` instead of string `topPosition` for better type safety and clearer intent.

Adds uncontrolled mode support to PasswordInput with internal state management and optional `defaultShowPassword` prop.

Removes unnecessary margin classes from various View components to clean up spacing.
2025-09-01 22:54:11 +02:00
Uruk
be884ce6e6 fix: improve list item height consistency and accessibility
Replaces fixed height with minimum height to ensure proper content display and accessibility compliance.

Changes list items from fixed 44px height to minimum height, allowing content to expand naturally while maintaining the minimum touch target size.

Adds specific styling to gesture control settings items to accommodate longer descriptions and improve readability.
2025-09-01 22:49:30 +02:00
Uruk
a88e13b14f refactor: streamline pull request template by removing unnecessary sections 2025-09-01 21:27:14 +02:00
Uruk
ff6b1112b6 feat: add consistent focus state styling to input component
Improves visual feedback by adding focus state borders to all input variants.

Changes the unfocused border from transparent to neutral-800 for better visual consistency and adds focus/blur handlers to ensure proper state management across different input types.
2025-09-01 21:20:40 +02:00
Uruk
1156942e33 fix: adjust password field top position for better layout
Updates the top position value from 4 to 15 to improve the visual spacing and alignment of the password input field in the TV layout mode.
2025-09-01 21:05:26 +02:00
Uruk
e9effb46f6 chore: pin devDependency versions to exact releases
Removes caret ranges from development dependencies to ensure consistent builds across environments and prevent unexpected version drift during installation.

Updates multiple packages including Babel, Biome, React Native CLI, TypeScript tooling, and testing utilities to their exact versions.
2025-09-01 20:57:49 +02:00
renovate[bot]
8a6c6dbd69 chore(deps): Update github/codeql-action action to v3.30.0 (#1021) 2025-09-01 20:33:25 +02:00
Uruk
4dce87dfd3 refactor: simplify PasswordInput component and standardize usage
Removes unnecessary props and internal state management from PasswordInput component to make it more focused and reusable. Wraps all PasswordInput instances in relative positioned Views for consistent layout behavior.

Updates package.json to use caret version for @expo/vector-icons dependency for better version flexibility.
2025-09-01 19:18:54 +02:00
Uruk
2f2e5a2730 feat: add customizable icon color to PasswordInput
Adds iconColor prop with white default to allow customization of the eye/eye-off toggle icon color.

Also simplifies top position class construction by using template literal instead of conditional logic.
2025-09-01 16:00:04 +02:00
Uruk
614736ad4a feat: upgrade @expo/vector-icons to v15.0.2
Updates the vector icons package to the latest major version, which includes improved peer dependency constraints for expo-font (>=14.0.4) and enhanced icon support for the application.
2025-09-01 15:59:12 +02:00
Uruk
3919bb346f refactor: replace inline password inputs with reusable PasswordInput component
Consolidates duplicate password input implementations across login and settings screens into a single reusable component.

Improves code maintainability by eliminating redundant password visibility toggle logic and standardizing password input behavior throughout the application.

Adds consistent accessibility support and test identifiers across all password input instances.
2025-09-01 15:48:55 +02:00
Uruk
87eff6f80c fix: adjust password toggle button vertical alignment for TV
Improves visual positioning of the show/hide password toggle button by slightly adjusting the top margin from 3.5 to 4, ensuring better alignment with the password input field.
2025-09-01 15:30:21 +02:00
Uruk
15d0de806b feat: add password visibility toggle to login forms
Improves user experience by allowing users to show/hide password text in both main login form and Jellyseerr settings.

Adds eye icon button that toggles between masked and visible password input, making it easier for users to verify their password entries.
2025-09-01 15:05:40 +02:00
80 changed files with 858 additions and 548 deletions

View File

@@ -5,8 +5,6 @@
and to ensure all necessary checks are completed before merging. and to ensure all necessary checks are completed before merging.
--> -->
# 📦 Pull Request
## 🔖 Summary ## 🔖 Summary
<!-- <!--
A concise description of the changes introduced by this PR. A concise description of the changes introduced by this PR.
@@ -36,12 +34,6 @@ Spec: https://www.conventionalcommits.org/ -->
- Short summary: what changed and why (12 lines) - Short summary: what changed and why (12 lines)
--> -->
## 📋 Details
<!--
Provide more context or background. Explain any non-obvious decisions.
Include screenshots or GIFs for UI changes if applicable.
-->
### ⚠️ Breaking Changes ### ⚠️ Breaking Changes
<!-- List any breaking API/contract changes and migration guidance. If none, write “None”. --> <!-- List any breaking API/contract changes and migration guidance. If none, write “None”. -->
@@ -52,7 +44,10 @@ Include screenshots or GIFs for UI changes if applicable.
<!-- Hot paths, memory/CPU/latency implications, benchmarks if available. --> <!-- Hot paths, memory/CPU/latency implications, benchmarks if available. -->
### 🖼️ Screenshots / GIFs (if UI) ### 🖼️ Screenshots / GIFs (if UI)
<!-- Before/After, dark mode, responsive states. --> <!--
Provide more context or background. Explain any non-obvious decisions.
Before/After, dark mode, responsive states.
-->
## ✅ Checklist ## ✅ Checklist
<!-- <!--

View File

@@ -31,13 +31,13 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: 🏁 Initialize CodeQL - name: 🏁 Initialize CodeQL
uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 uses: github/codeql-action/init@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality queries: +security-extended,security-and-quality
- name: 🛠️ Autobuild - name: 🛠️ Autobuild
uses: github/codeql-action/autobuild@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 uses: github/codeql-action/autobuild@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
- name: 🧪 Perform CodeQL Analysis - name: 🧪 Perform CodeQL Analysis
uses: github/codeql-action/analyze@3c3833e0f8c1c83d449a7478aa59c036a9165498 # v3.29.11 uses: github/codeql-action/analyze@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Streamyfin", "name": "Streamyfin",
"slug": "streamyfin", "slug": "streamyfin",
"version": "0.35.0", "version": "0.35.1",
"orientation": "default", "orientation": "default",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "streamyfin", "scheme": "streamyfin",
@@ -37,7 +37,7 @@
}, },
"android": { "android": {
"jsEngine": "hermes", "jsEngine": "hermes",
"versionCode": 66, "versionCode": 67,
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png", "foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png", "monochromeImage": "./assets/images/icon-android-themed.png",

View File

@@ -12,7 +12,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
export default function page() { export default function page() {
const [settings, updateSettings, pluginSettings] = useSettings(null); const { settings, updateSettings, pluginSettings } = useSettings();
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);

View File

@@ -3,7 +3,7 @@ import { JellyseerrSettings } from "@/components/settings/Jellyseerr";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
export default function page() { export default function page() {
const [_settings, _updateSettings, pluginSettings] = useSettings(null); const { pluginSettings } = useSettings();
return ( return (
<DisabledSetting <DisabledSetting

View File

@@ -21,7 +21,7 @@ export default function page() {
const { t } = useTranslation(); const { t } = useTranslation();
const [settings, updateSettings, pluginSettings] = useSettings(null); const { settings, updateSettings, pluginSettings } = useSettings();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [value, setValue] = useState<string>(settings?.marlinServerUrl || ""); const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");

View File

@@ -15,7 +15,7 @@ import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Dis
export default function page() { export default function page() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
const { companyId, image, type } = local as unknown as { const { companyId, image, type } = local as unknown as {
companyId: string; companyId: string;
@@ -53,7 +53,10 @@ export default function page() {
uniqBy( uniqBy(
data?.pages data?.pages
?.filter((p) => p?.results.length) ?.filter((p) => p?.results.length)
.flatMap((p) => p?.results ?? []), .flatMap(
(p) =>
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [],
),
"id", "id",
) ?? [], ) ?? [],
[data], [data],
@@ -98,9 +101,7 @@ export default function page() {
}} }}
/> />
} }
renderItem={(item, _index) => ( renderItem={(item, _index) => <JellyseerrPoster item={item} />}
<JellyseerrPoster item={item as MovieResult | TvResult} />
)}
/> />
); );
} }

View File

@@ -8,14 +8,10 @@ import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import {
type MovieResult,
type TvResult,
} from "@/utils/jellyseerr/server/models/Search";
export default function page() { export default function page() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
const { genreId, name, type } = local as unknown as { const { genreId, name, type } = local as unknown as {
genreId: string; genreId: string;
@@ -51,7 +47,10 @@ export default function page() {
uniqBy( uniqBy(
data?.pages data?.pages
?.filter((p) => p?.results.length) ?.filter((p) => p?.results.length)
.flatMap((p) => p?.results ?? []), .flatMap(
(p) =>
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [],
),
"id", "id",
) ?? [], ) ?? [],
[data], [data],
@@ -62,7 +61,7 @@ export default function page() {
jellyseerrApi jellyseerrApi
? flatData.map((r) => ? flatData.map((r) =>
jellyseerrApi.imageProxy( jellyseerrApi.imageProxy(
(r as TvResult | MovieResult).backdropPath, r.backdropPath,
"w1920_and_h800_multi_faces", "w1920_and_h800_multi_faces",
), ),
) )
@@ -92,9 +91,7 @@ export default function page() {
{name} {name}
</Text> </Text>
} }
renderItem={(item, _index) => ( renderItem={(item, _index) => <JellyseerrPoster item={item} />}
<JellyseerrPoster item={item as MovieResult | TvResult} />
)}
/> />
); );
} }

View File

@@ -10,10 +10,6 @@ import { OverviewText } from "@/components/OverviewText";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
export default function page() { export default function page() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
@@ -106,9 +102,7 @@ export default function page() {
MainContent={() => ( MainContent={() => (
<OverviewText text={data?.details?.biography} className='mt-4' /> <OverviewText text={data?.details?.biography} className='mt-4' />
)} )}
renderItem={(item, _index) => ( renderItem={(item, _index) => <JellyseerrPoster item={item} />}
<JellyseerrPoster item={item as MovieResult | TvResult} />
)}
/> />
); );
} }

View File

@@ -7,7 +7,7 @@ import { useAtom } from "jotai";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll"; import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorizontalScroll";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText"; import { ItemCardText } from "@/components/ItemCardText";
@@ -22,21 +22,21 @@ import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
const page: React.FC = () => { const page: React.FC = () => {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
const { actorId } = local as { actorId: string }; const { personId } = local as { personId: string };
const { t } = useTranslation(); const { t } = useTranslation();
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { data: item, isLoading: l1 } = useQuery({ const { data: item, isLoading: l1 } = useQuery({
queryKey: ["item", actorId], queryKey: ["item", personId],
queryFn: async () => queryFn: async () =>
await getUserItemData({ await getUserItemData({
api, api,
userId: user?.Id, userId: user?.Id,
itemId: actorId, itemId: personId,
}), }),
enabled: !!actorId && !!api, enabled: !!personId && !!api,
staleTime: 60, staleTime: 60,
}); });
@@ -50,7 +50,7 @@ const page: React.FC = () => {
const response = await getItemsApi(api).getItems({ const response = await getItemsApi(api).getItems({
userId: user.Id, userId: user.Id,
personIds: [actorId], personIds: [personId],
startIndex: pageParam, startIndex: pageParam,
limit: 16, limit: 16,
sortOrder: ["Descending", "Descending", "Ascending"], sortOrder: ["Descending", "Descending", "Ascending"],
@@ -68,7 +68,7 @@ const page: React.FC = () => {
return response.data; return response.data;
}, },
[api, user?.Id, actorId], [api, user?.Id, personId],
); );
const backdropUrl = useMemo( const backdropUrl = useMemo(
@@ -131,7 +131,7 @@ const page: React.FC = () => {
</TouchableItemRouter> </TouchableItemRouter>
)} )}
queryFn={fetchItems} queryFn={fetchItems}
queryKey={["actor", "movies", actorId]} queryKey={["actor", "movies", personId]}
/> />
<View className='h-12' /> <View className='h-12' />
</View> </View>

View File

@@ -9,7 +9,7 @@ const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function IndexLayout() { export default function IndexLayout() {
const [settings, updateSettings, pluginSettings] = useSettings(null); const { settings, updateSettings, pluginSettings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -19,7 +19,7 @@ export default function index() {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [settings] = useSettings(null); const { settings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -71,7 +71,7 @@ export default function search() {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [settings] = useSettings(null); const { settings } = useSettings();
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi } = useJellyseerr();
const [jellyseerrOrderBy, setJellyseerrOrderBy] = const [jellyseerrOrderBy, setJellyseerrOrderBy] =
useState<JellyseerrSearchSort>( useState<JellyseerrSearchSort>(

View File

@@ -27,7 +27,7 @@ export const NativeTabs = withLayoutContext<
>(Navigator); >(Navigator);
export default function TabLayout() { export default function TabLayout() {
const [settings] = useSettings(null); const { settings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();

View File

@@ -97,7 +97,7 @@ export default function page() {
/** Playback position in ticks. */ /** Playback position in ticks. */
playbackPosition?: string; playbackPosition?: string;
}>(); }>();
const [_settings] = useSettings(null); useSettings();
const offline = offlineStr === "true"; const offline = offlineStr === "true";
const playbackManager = usePlaybackManager(); const playbackManager = usePlaybackManager();
@@ -748,7 +748,6 @@ export default function page() {
setAspectRatio={setAspectRatio} setAspectRatio={setAspectRatio}
setScaleFactor={setScaleFactor} setScaleFactor={setScaleFactor}
isVlc isVlc
api={api}
downloadedFiles={downloadedFiles} downloadedFiles={downloadedFiles}
/> />
)} )}

View File

@@ -230,7 +230,7 @@ const queryClient = new QueryClient({
}); });
function Layout() { function Layout() {
const [settings] = useSettings(null); const { settings } = useSettings();
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const appState = useRef(AppState.currentState); const appState = useRef(AppState.currentState);
@@ -269,6 +269,15 @@ function Layout() {
await Notifications?.setNotificationChannelAsync("default", { await Notifications?.setNotificationChannelAsync("default", {
name: "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,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF231F7C",
});
} }
const granted = await checkAndRequestPermissions(); const granted = await checkAndRequestPermissions();

View File

@@ -20,6 +20,7 @@ import { Button } from "@/components/Button";
import { Input } from "@/components/common/Input"; import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery"; import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
import { PasswordInput } from "@/components/PasswordInput";
import { PreviousServersList } from "@/components/PreviousServersList"; import { PreviousServersList } from "@/components/PreviousServersList";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider"; import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
@@ -52,6 +53,7 @@ const Login: React.FC = () => {
username: _username, username: _username,
password: _password, password: _password,
}); });
const [showPassword, setShowPassword] = useState<boolean>(false);
/** /**
* A way to auto login based on a link * A way to auto login based on a link
@@ -271,30 +273,27 @@ const Login: React.FC = () => {
textContentType='oneTimeCode' textContentType='oneTimeCode'
clearButtonMode='while-editing' clearButtonMode='while-editing'
maxLength={500} maxLength={500}
extraClassName='mb-4' extraClassName='mb-2'
/> />
{/* Password */} {/* Password */}
<Input <View className='relative mb-2'>
placeholder={t("login.password_placeholder")} <PasswordInput
onChangeText={(text: string) => value={credentials.password}
setCredentials({ ...credentials, password: text }) onChangeText={(text: string) =>
} setCredentials({ ...credentials, password: text })
value={credentials.password} }
secureTextEntry placeholder={t("login.password_placeholder")}
keyboardType='default' showPassword={showPassword}
returnKeyType='done' onShowPasswordChange={setShowPassword}
autoCapitalize='none' topOffset={16}
textContentType='password' layout='tv'
clearButtonMode='while-editing' />
maxLength={500}
extraClassName='mb-4'
/>
<View className='mt-4'>
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
</View> </View>
<View className='mt-3'> <View className='mt-3'>
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
</View>
<View className='mt-2'>
<Button <Button
onPress={handleQuickConnect} onPress={handleQuickConnect}
className='bg-neutral-800 border border-neutral-700' className='bg-neutral-800 border border-neutral-700'
@@ -348,7 +347,7 @@ const Login: React.FC = () => {
</View> </View>
{/* Lists stay full width but inside max width container */} {/* Lists stay full width but inside max width container */}
<View className='mt-2'> <View className='mt-4'>
<JellyfinServerDiscovery <JellyfinServerDiscovery
onServerSelect={async (server: any) => { onServerSelect={async (server: any) => {
setServerURL(server.address); setServerURL(server.address);
@@ -402,22 +401,22 @@ const Login: React.FC = () => {
textContentType='oneTimeCode' textContentType='oneTimeCode'
clearButtonMode='while-editing' clearButtonMode='while-editing'
maxLength={500} maxLength={500}
extraClassName=''
/> />
<Input <View className='relative'>
placeholder={t("login.password_placeholder")} <PasswordInput
onChangeText={(text) => value={credentials.password}
setCredentials({ ...credentials, password: text }) onChangeText={(text) =>
} setCredentials({ ...credentials, password: text })
value={credentials.password} }
secureTextEntry placeholder={t("login.password_placeholder")}
keyboardType='default' showPassword={showPassword}
returnKeyType='done' onShowPasswordChange={setShowPassword}
autoCapitalize='none' topOffset={12}
textContentType='password' layout='mobile'
clearButtonMode='while-editing' />
maxLength={500} </View>
/>
<View className='flex flex-row items-center justify-between'> <View className='flex flex-row items-center justify-between'>
<Button <Button
onPress={handleLogin} onPress={handleLogin}
@@ -428,7 +427,7 @@ const Login: React.FC = () => {
</Button> </Button>
<TouchableOpacity <TouchableOpacity
onPress={handleQuickConnect} onPress={handleQuickConnect}
className='p-2 bg-neutral-900 rounded-xl h-12 w-12 flex items-center justify-center' className='p-3 bg-neutral-900 rounded-xl h-13 w-13 flex items-center justify-center'
> >
<MaterialCommunityIcons <MaterialCommunityIcons
name='cellphone-lock' name='cellphone-lock'

182
bun.lock
View File

@@ -7,7 +7,7 @@
"@bottom-tabs/react-navigation": "^0.9.2", "@bottom-tabs/react-navigation": "^0.9.2",
"@expo/metro-runtime": "~5.0.4", "@expo/metro-runtime": "~5.0.4",
"@expo/react-native-action-sheet": "^4.1.1", "@expo/react-native-action-sheet": "^4.1.1",
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.1.0", "@gorhom/bottom-sheet": "^5.1.0",
"@jellyfin/sdk": "^0.11.0", "@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.6", "@kesha-antonov/react-native-background-downloader": "^3.2.6",
@@ -76,33 +76,33 @@
"react-native-video": "6.14.1", "react-native-video": "6.14.1",
"react-native-volume-manager": "^2.0.8", "react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.20.0", "react-native-web": "^0.20.0",
"sonner-native": "^0.21.0", "sonner-native": "^0.21.1",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"zeego": "^3.0.6", "zeego": "^3.0.6",
"zod": "^4.1.3", "zod": "^4.1.3",
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "7.28.3",
"@biomejs/biome": "^2.2.2", "@biomejs/biome": "2.2.2",
"@react-native-community/cli": "^20.0.0", "@react-native-community/cli": "20.0.1",
"@react-native-tvos/config-tv": "^0.1.1", "@react-native-tvos/config-tv": "0.1.3",
"@types/jest": "^29.5.12", "@types/jest": "29.5.14",
"@types/lodash": "^4.17.15", "@types/lodash": "4.17.20",
"@types/react": "~19.0.10", "@types/react": "~19.0.10",
"@types/react-test-renderer": "^19.0.0", "@types/react-test-renderer": "19.1.0",
"cross-env": "^10.0.0", "cross-env": "10.0.0",
"expo-doctor": "^1.17.0", "expo-doctor": "1.17.2",
"husky": "^9.1.7", "husky": "9.1.7",
"lint-staged": "^16.1.5", "lint-staged": "16.1.6",
"postinstall-postinstall": "^2.1.0", "postinstall": "0.11.2",
"react-test-renderer": "19.1.1", "react-test-renderer": "19.1.1",
"typescript": "~5.8.3", "typescript": "5.8.3",
}, },
}, },
}, },
"trustedDependencies": [ "trustedDependencies": [
"postinstall-postinstall", "postinstall",
], ],
"packages": { "packages": {
"@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="],
@@ -317,6 +317,8 @@
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.9.2", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-IZZKllcaqCGsKIgeXmYFGU95IXxbBpXtwKws4Lg2GJw/qqAYYsPFEl0JBvnymSD7G1zkHYEilg5UHuTd0NmX7A=="], "@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.9.2", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-IZZKllcaqCGsKIgeXmYFGU95IXxbBpXtwKws4Lg2GJw/qqAYYsPFEl0JBvnymSD7G1zkHYEilg5UHuTd0NmX7A=="],
"@danieldietrich/copy": ["@danieldietrich/copy@0.4.2", "", {}, "sha512-ZVNZIrgb2KeomfNahP77rL445ho6aQj0HHqU6hNlQ61o4rhvca+NS+ePj0d82zQDq2UPk1mjVZBTXgP+ErsDgw=="],
"@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="], "@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="],
"@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="],
@@ -367,7 +369,7 @@
"@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="], "@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="],
"@expo/vector-icons": ["@expo/vector-icons@14.1.0", "", { "peerDependencies": { "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ=="], "@expo/vector-icons": ["@expo/vector-icons@15.0.2", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-IiBjg7ZikueuHNf40wSGCf0zS73a3guJLdZzKnDUxsauB8VWPLMeWnRIupc+7cFhLUkqyvyo0jLNlcxG5xPOuQ=="],
"@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="], "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="],
@@ -391,6 +393,10 @@
"@ide/backoff": ["@ide/backoff@1.0.0", "", {}, "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g=="], "@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/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/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
@@ -511,29 +517,29 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@react-native-community/cli": ["@react-native-community/cli@20.0.0", "", { "dependencies": { "@react-native-community/cli-clean": "20.0.0", "@react-native-community/cli-config": "20.0.0", "@react-native-community/cli-doctor": "20.0.0", "@react-native-community/cli-server-api": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "@react-native-community/cli-types": "20.0.0", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-/cMnGl5V1rqnbElY1Fvga1vfw0d3bnqiJLx2+2oh7l9ulnXfVRWb5tU2kgBqiMxuDOKA+DQoifC9q/tvkj5K2w=="], "@react-native-community/cli": ["@react-native-community/cli@20.0.1", "", { "dependencies": { "@react-native-community/cli-clean": "20.0.1", "@react-native-community/cli-config": "20.0.1", "@react-native-community/cli-doctor": "20.0.1", "@react-native-community/cli-server-api": "20.0.1", "@react-native-community/cli-tools": "20.0.1", "@react-native-community/cli-types": "20.0.1", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-Q7UnBqOO/JsWfgmO9qZjrKgMi/0U9ih0FywXXheml8VH1hn/pBXKIeO/BvzA6g5gHIvBZ/6KyhdGoNok1R/ZJw=="],
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-YmdNRcT+Dp8lC7CfxSDIfPMbVPEXVFzBH62VZNbYGxjyakqAvoQUFTYPgM2AyFusAr4wDFbDOsEv88gCDwR3ig=="], "@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.0.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.1", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-/4Q28dDz9k++cbTSDiEQ+i2n4XlRy1keXi8VIpGVTRxAuxX+KNtNlCk5MnmPoiOZDvaRRc7xTUebUCMC03HFeQ=="],
"@react-native-community/cli-config": ["@react-native-community/cli-config@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1" } }, "sha512-5Ky9ceYuDqG62VIIpbOmkg8Lybj2fUjf/5wK4UO107uRqejBgNgKsbGnIZgEhREcaSEOkujWrroJ9gweueLfBg=="], "@react-native-community/cli-config": ["@react-native-community/cli-config@20.0.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.1", "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1" } }, "sha512-DMjbgqFcWlCmoDAn9CgcPoEMMfRE0HgCECAeVvby+DOtcses/QxTPX1L9s2hZU16AOUSNxZEXqLzAw7pqGJl0A=="],
"@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1" } }, "sha512-asv60qYCnL1v0QFWcG9r1zckeFlKG+14GGNyPXY72Eea7RX5Cxdx8Pb6fIPKroWH1HEWjYH9KKHksMSnf9FMKw=="], "@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.0.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.1", "chalk": "^4.1.2", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1" } }, "sha512-2Opfc38FZq2chMXUSY75Fzcgwe2G96sd1O4sAkq7UeoP6HGjAT2hh4xu3lzTmevUhS0T5EzIDUdYT55wTJf60A=="],
"@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-PS1gNOdpeQ6w7dVu1zi++E+ix2D0ZkGC2SQP6Y/Qp002wG4se56esLXItYiiLrJkhH21P28fXdmYvTEkjSm9/Q=="], "@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.0.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.1", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-gtNRlNQxhA57y3vRxjTkHPusLEkLQqqkHOOE0LLeTuMQ/X8q0tdPKfFjdHDXSwjz4wXkDN327kxTPx0UPqohhg=="],
"@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.0.0", "", { "dependencies": { "@react-native-community/cli-config": "20.0.0", "@react-native-community/cli-platform-android": "20.0.0", "@react-native-community/cli-platform-apple": "20.0.0", "@react-native-community/cli-platform-ios": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "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", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-cPHspi59+Fy41FDVxt62ZWoicCZ1o34k8LAl64NVSY0lwPl+CEi78jipXJhtfkVqSTetloA8zexa/vSAcJy57Q=="], "@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.0.1", "", { "dependencies": { "@react-native-community/cli-config": "20.0.1", "@react-native-community/cli-platform-android": "20.0.1", "@react-native-community/cli-platform-apple": "20.0.1", "@react-native-community/cli-platform-ios": "20.0.1", "@react-native-community/cli-tools": "20.0.1", "chalk": "^4.1.2", "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", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-ADzNiHpbq/CUtjJEBGQ8KxwZv7JbyIXVnsatMlP/xP5A+FbMq4YOA2FyjqSpfqha60yRMqSkc5tTLdIKtsC5yA=="],
"@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.0.0", "", { "dependencies": { "@react-native-community/cli-config-android": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "logkitty": "^0.7.1" } }, "sha512-th3ji1GRcV6ACelgC0wJtt9daDZ+63/52KTwL39xXGoqczFjml4qERK90/ppcXU0Ilgq55ANF8Pr+UotQ2AB/A=="], "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.0.1", "", { "dependencies": { "@react-native-community/cli-config-android": "20.0.1", "@react-native-community/cli-tools": "20.0.1", "chalk": "^4.1.2", "execa": "^5.0.0", "logkitty": "^0.7.1" } }, "sha512-Rfyi1J+TKTiDjiZ2JpodwQHp5ETv7MWoOcWLK4JeeBHQLCYlVII+7RSg44tFc0JC2/bCXscqfQOR1aYZr0sPTg=="],
"@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1" } }, "sha512-rZZCnAjUHN1XBgiWTAMwEKpbVTO4IHBSecdd1VxJFeTZ7WjmstqA6L/HXcnueBgxrzTCRqvkRIyEQXxC1OfhGw=="], "@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.0.1", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.0.1", "@react-native-community/cli-tools": "20.0.1", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1" } }, "sha512-pzPsdmYhVrg9oluL0ofWopVrbzYOJg+RBoJuoBOjpI7JWUduS8A3uLUAR8ysT+lo1x0+aztD4App9nKyY+vfbw=="],
"@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.0.0", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.0.0" } }, "sha512-Z35M+4gUJgtS4WqgpKU9/XYur70nmj3Q65c9USyTq6v/7YJ4VmBkmhC9BticPs6wuQ9Jcv0NyVCY0Wmh6kMMYw=="], "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.0.1", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.0.1" } }, "sha512-kvpUBcqOC5CE8xWCoimyP2M/gr3TVeoLRKhliixnpx7fc9cB6R6GWHgzrNDVf4z5SAQW64UFvswquPo3dk+YOg=="],
"@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "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", "ws": "^6.2.3" } }, "sha512-Ves21bXtjUK3tQbtqw/NdzpMW1vR2HvYCkUQ/MXKrJcPjgJnXQpSnTqHXz6ZdBlMbbwLJXOhSPiYzxb5/v4CDg=="], "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.0.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.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", "ws": "^6.2.3" } }, "sha512-x5wr71LJzVBGLVLFPU9iGBkY1Raw2RCd1KDKKnbYMbX/KiAgYnhW3flF0RIX+LpIkdZ8hIIcj7SETaByAAR1FA=="],
"@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-akSZGxr1IajJ8n0YCwQoA3DI0HttJ0WB7M3nVpb0lOM+rJpsBN7WG5Ft+8ozb6HyIPX+O+lLeYazxn5VNG/Xhw=="], "@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.0.1", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-yrSOkVfGm8yG88DRc4DjfM4XFmRpIXGkB1StKfU8aUPzO5Pbp8cobYYdsCeK234vp9/SZu535uRrno6Or53+Jw=="],
"@react-native-community/cli-types": ["@react-native-community/cli-types@20.0.0", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-7J4hzGWOPTBV1d30Pf2NidV+bfCWpjfCOiGO3HUhz1fH4MvBM0FbbBmE9LE5NnMz7M8XSRSi68ZGYQXgLBB2Qw=="], "@react-native-community/cli-types": ["@react-native-community/cli-types@20.0.1", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-nvIk3axp9gXyZ3Ri4UrfiCam8bYWOL0z4B1pOhSKW/9wrg2v2E9SK27xf3GZGAP/XuWKT7tuyRWHx1KZEQsfkg=="],
"@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@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="],
@@ -1009,7 +1015,7 @@
"expo-device": ["expo-device@7.1.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-HS04IiE1Fy0FRjBLurr9e5A6yj3kbmQB+2jCZvbSGpsjBnCLdSk/LCii4f5VFhPIBWJLyYuN5QqJyEAw6BcS4Q=="], "expo-device": ["expo-device@7.1.4", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-HS04IiE1Fy0FRjBLurr9e5A6yj3kbmQB+2jCZvbSGpsjBnCLdSk/LCii4f5VFhPIBWJLyYuN5QqJyEAw6BcS4Q=="],
"expo-doctor": ["expo-doctor@1.17.0", "", { "bin": { "expo-doctor": "build/index.js" } }, "sha512-t+cweyCKbDE+ORgNh8CCybRXFwae/uJKfPetirDUF+PjaZvc6PPVF8gwBD+nFQ1dLURjxD4IaVNwC94Iyg6q0A=="], "expo-doctor": ["expo-doctor@1.17.2", "", { "bin": { "expo-doctor": "build/index.js" } }, "sha512-GzrOscosrvmXx/e90uL1XYWEuqcaVVZppQ0VySE30BpXyGAlj80AAs2n/+x4M9JaeZIhi0aWNGI7lhCTN0pedw=="],
"expo-file-system": ["expo-file-system@18.1.11", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ=="], "expo-file-system": ["expo-file-system@18.1.11", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ=="],
@@ -1133,7 +1139,7 @@
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "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.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -1243,7 +1249,7 @@
"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=="], "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.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
"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-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=="],
@@ -1331,9 +1337,9 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"lint-staged": ["lint-staged@16.1.5", "", { "dependencies": { "chalk": "^5.5.0", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^9.0.1", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A=="], "lint-staged": ["lint-staged@16.1.6", "", { "dependencies": { "chalk": "^5.6.0", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^9.0.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow=="],
"listr2": ["listr2@9.0.1", "", { "dependencies": { "cli-truncate": "^4.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-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g=="], "listr2": ["listr2@9.0.3", "", { "dependencies": { "cli-truncate": "^4.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-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@@ -1409,7 +1415,7 @@
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "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=="], "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -1511,7 +1517,7 @@
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"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=="], "path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
@@ -1553,7 +1559,7 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"postinstall-postinstall": ["postinstall-postinstall@2.1.0", "", {}, "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ=="], "postinstall": ["postinstall@0.11.2", "", { "dependencies": { "@danieldietrich/copy": "^0.4.2", "glob": "^11.0.2", "minimist": "^1.2.8", "resolve": "^1.22.10" }, "bin": { "postinstall": "bin/postinstall.js" } }, "sha512-Et2ClU0WofEsK14Lc8xeGB1Vor9R6/+pB9jPQ9AW5Rpbt8By/yv7CaIh+qWgKJ2yy06kTJ+tIICo7cNQc65/Tg=="],
"pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
@@ -1781,7 +1787,7 @@
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
"sonner-native": ["sonner-native@0.21.0", "", { "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-pIdyMd722xuDukIvCZCmWh9Jy5FV+m9uKYl3oAzonYE+91eX5556vYrIik5MisDlBniSXaWqC9CnPoS6DKo2Lg=="], "sonner-native": ["sonner-native@0.21.1", "", { "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-00RSmfVBd/XfQdRh7sqgFUjftx09HRgEMnZei4CVKcRKeqRcq9DXn5o1nJhz3aA4Cyf5k2+0kK4spdWtAtNqSA=="],
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
@@ -2007,6 +2013,8 @@
"@expo/cli/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "@expo/cli/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"@expo/cli/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@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/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/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="],
@@ -2041,6 +2049,8 @@
"@expo/fingerprint/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "@expo/fingerprint/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"@expo/fingerprint/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@expo/fingerprint/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@expo/fingerprint/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@expo/image-utils/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], "@expo/image-utils/getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
@@ -2053,6 +2063,8 @@
"@expo/metro-config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "@expo/metro-config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"@expo/metro-config/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@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/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
"@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], "@expo/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=="],
@@ -2091,6 +2103,8 @@
"@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.79.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ=="], "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.79.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ=="],
"@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.79.5", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.79.5", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw=="], "@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.79.5", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.79.5", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw=="],
"@react-native/community-cli-plugin/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "@react-native/community-cli-plugin/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -2137,6 +2151,8 @@
"error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"expo/@expo/vector-icons": ["@expo/vector-icons@14.1.0", "", { "peerDependencies": { "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ=="],
"expo-build-properties/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "expo-build-properties/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"expo-dev-launcher/ajv": ["ajv@8.11.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg=="], "expo-dev-launcher/ajv": ["ajv@8.11.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg=="],
@@ -2163,8 +2179,6 @@
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "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=="], "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=="], "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -2177,7 +2191,7 @@
"lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"lint-staged/chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="], "lint-staged/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="],
"lint-staged/commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], "lint-staged/commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="],
@@ -2207,7 +2221,7 @@
"npm-package-arg/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "npm-package-arg/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "path-scurry/lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
"postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -2225,6 +2239,8 @@
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"react-native/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "react-native/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
"react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -2241,6 +2257,8 @@
"requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="], "requireg/resolve": ["resolve@1.7.1", "", { "dependencies": { "path-parse": "^1.0.5" } }, "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw=="],
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/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=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
@@ -2271,6 +2289,8 @@
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -2289,6 +2309,10 @@
"@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
"@expo/cli/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"@expo/cli/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=="],
"@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
"@expo/cli/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], "@expo/cli/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="],
@@ -2297,6 +2321,32 @@
"@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], "@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
"@expo/config-plugins/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"@expo/config-plugins/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@expo/config-plugins/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=="],
"@expo/config/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"@expo/config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@expo/config/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=="],
"@expo/devcert/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"@expo/devcert/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@expo/devcert/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=="],
"@expo/fingerprint/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"@expo/fingerprint/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=="],
"@expo/metro-config/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"@expo/metro-config/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=="],
"@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
"@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], "@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="],
@@ -2315,6 +2365,10 @@
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.79.5", "", {}, "sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A=="], "@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.79.5", "", {}, "sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A=="],
"@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], "@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
@@ -2343,9 +2397,13 @@
"connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "expo-modules-autolinking/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"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=="], "expo-modules-autolinking/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"expo-modules-autolinking/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=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -2377,8 +2435,12 @@
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "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/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -2387,6 +2449,12 @@
"serve-static/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], "serve-static/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"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=="], "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=="], "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=="],
@@ -2399,6 +2467,8 @@
"@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
"@expo/cli/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -2409,6 +2479,16 @@
"@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], "@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
"@expo/config-plugins/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@expo/config/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@expo/devcert/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@expo/fingerprint/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@expo/metro-config/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -2421,12 +2501,18 @@
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"ansi-fragments/slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "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=="],
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"expo-modules-autolinking/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -2441,8 +2527,14 @@
"metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "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=="],
"@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"@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=="], "@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=="],
@@ -2459,6 +2551,8 @@
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"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=="], "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=="],

View File

@@ -61,7 +61,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const [queue, _setQueue] = useAtom(queueAtom); const [queue, _setQueue] = useAtom(queueAtom);
const [settings] = useSettings(null); const { settings } = useSettings();
const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false); const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
const { processes, startBackgroundDownload, getDownloadedItems } = const { processes, startBackgroundDownload, getDownloadedItems } =

View File

@@ -54,7 +54,7 @@ interface ItemContentProps {
export const ItemContent: React.FC<ItemContentProps> = React.memo( export const ItemContent: React.FC<ItemContentProps> = React.memo(
({ item, isOffline }) => { ({ item, isOffline }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [settings] = useSettings(null); const { settings } = useSettings();
const { orientation } = useOrientation(); const { orientation } = useOrientation();
const navigation = useNavigation(); const navigation = useNavigation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();

View File

@@ -5,7 +5,7 @@ import { useAtom } from "jotai";
import type React from "react"; import type React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; import { HorizontalScroll } from "@/components/common/HorizontalScroll";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText"; import { ItemCardText } from "@/components/ItemCardText";

View File

@@ -0,0 +1,111 @@
import { Ionicons } from "@expo/vector-icons";
import type React from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { TouchableOpacity } from "react-native";
import { Input } from "./common/Input";
// Discriminated union for password visibility control
type PasswordVisibilityControlled = {
value?: string;
onChangeText: (text: string) => void;
placeholder: string;
showPassword: boolean;
onShowPasswordChange: (show: boolean) => void;
topOffset?: number;
layout?: "tv" | "mobile";
};
type PasswordVisibilityUncontrolled = {
value?: string;
onChangeText: (text: string) => void;
placeholder: string;
showPassword?: never;
onShowPasswordChange?: never;
topOffset?: number;
layout?: "tv" | "mobile";
defaultShowPassword?: boolean;
};
type PasswordInputProps =
| PasswordVisibilityControlled
| PasswordVisibilityUncontrolled;
export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
const { t } = useTranslation();
const {
value = "",
onChangeText,
placeholder,
topOffset = 14, // Default 14px for mobile
layout = "mobile",
} = props;
// Type guard to check if we're in controlled mode
const isControlled =
"showPassword" in props && "onShowPasswordChange" in props;
// Internal state for uncontrolled mode
const [internalShowPassword, setInternalShowPassword] = useState(() =>
!isControlled && "defaultShowPassword" in props
? ((props as PasswordVisibilityUncontrolled).defaultShowPassword ?? false)
: false,
);
// Use controlled value if available, otherwise use internal state
const showPassword = isControlled
? (props as PasswordVisibilityControlled).showPassword
: internalShowPassword;
const handleTogglePassword = () => {
if (isControlled) {
(props as PasswordVisibilityControlled).onShowPasswordChange(
!showPassword,
);
} else {
// For uncontrolled mode, toggle internal state
setInternalShowPassword(!showPassword);
}
};
// Generate top position style with validation
const getTopStyle = () => {
if (typeof topOffset !== "number" || Number.isNaN(topOffset)) {
console.warn(`Invalid topOffset value: ${topOffset}`);
return { top: 14 }; // Default fallback (14px for mobile)
}
return { top: topOffset };
};
return (
<>
<Input
placeholder={placeholder}
onChangeText={onChangeText}
value={value}
secureTextEntry={!showPassword}
extraClassName='pr-4'
/>
<TouchableOpacity
onPress={handleTogglePassword}
className={`absolute right-3 p-1 ${
layout === "tv" ? "h-10 justify-center" : ""
}`}
style={getTopStyle()}
accessible={true}
accessibilityRole='button'
accessibilityLabel={
showPassword ? t("login.hide_password") : t("login.show_password")
}
accessibilityHint={t("login.toggle_password_visibility")}
accessibilityState={{ checked: showPassword }}
>
<Ionicons
name={showPassword ? "eye-off" : "eye"}
size={24}
color='white'
/>
</TouchableOpacity>
</>
);
};

View File

@@ -67,7 +67,7 @@ export const PlayButton: React.FC<Props> = ({
const startColor = useSharedValue(colorAtom); const startColor = useSharedValue(colorAtom);
const widthProgress = useSharedValue(0); const widthProgress = useSharedValue(0);
const colorChangeProgress = useSharedValue(0); const colorChangeProgress = useSharedValue(0);
const [settings, updateSettings] = useSettings(null); const { settings, updateSettings } = useSettings();
const lightHapticFeedback = useHaptic("light"); const lightHapticFeedback = useHaptic("light");
const goToPlayer = useCallback( const goToPlayer = useCallback(

View File

@@ -44,7 +44,7 @@ export const PlayButton: React.FC<Props> = ({
const startColor = useSharedValue(colorAtom); const startColor = useSharedValue(colorAtom);
const widthProgress = useSharedValue(0); const widthProgress = useSharedValue(0);
const colorChangeProgress = useSharedValue(0); const colorChangeProgress = useSharedValue(0);
const [settings] = useSettings(null); const { settings } = useSettings();
const lightHapticFeedback = useHaptic("light"); const lightHapticFeedback = useHaptic("light");
const goToPlayer = useCallback( const goToPlayer = useCallback(

View File

@@ -3,6 +3,7 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useMMKVString } from "react-native-mmkv"; import { useMMKVString } from "react-native-mmkv";
import { storage } from "@/utils/mmkv";
import { ListGroup } from "./list/ListGroup"; import { ListGroup } from "./list/ListGroup";
import { ListItem } from "./list/ListItem"; import { ListItem } from "./list/ListItem";
@@ -17,8 +18,7 @@ interface PreviousServersListProps {
export const PreviousServersList: React.FC<PreviousServersListProps> = ({ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
onServerSelect, onServerSelect,
}) => { }) => {
const [_previousServers, setPreviousServers] = const [_previousServers] = useMMKVString("previousServers");
useMMKVString("previousServers");
const previousServers = useMemo(() => { const previousServers = useMemo(() => {
return JSON.parse(_previousServers || "[]") as Server[]; return JSON.parse(_previousServers || "[]") as Server[];
@@ -37,14 +37,16 @@ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
onPress={() => onServerSelect(s)} onPress={() => onServerSelect(s)}
title={s.address} title={s.address}
showArrow showArrow
className='min-h-[48px] py-2'
/> />
))} ))}
<ListItem <ListItem
onPress={() => { onPress={() => {
setPreviousServers("[]"); storage.delete("previousServers");
}} }}
title={t("server.clear_button")} title={t("server.clear_button")}
textColor='red' textColor='red'
className='min-h-[48px] py-2'
/> />
</ListGroup> </ListGroup>
</View> </View>

View File

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import MoviePoster from "@/components/posters/MoviePoster"; import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { HorizontalScroll } from "./common/HorrizontalScroll"; import { HorizontalScroll } from "./common/HorizontalScroll";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { TouchableItemRouter } from "./common/TouchableItemRouter"; import { TouchableItemRouter } from "./common/TouchableItemRouter";
import { ItemCardText } from "./ItemCardText"; import { ItemCardText } from "./ItemCardText";

View File

@@ -20,8 +20,8 @@ export function Input(props: InputProps) {
<TextInput <TextInput
ref={inputRef} ref={inputRef}
className={` className={`
w-full text-lg px-5 py-4 rounded-2xl w-full text-lg px-5 py-5 rounded-2xl
${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-transparent"} ${isFocused ? "bg-neutral-700 border-2 border-white" : "bg-neutral-900 border-2 border-neutral-800"}
text-white ${extraClassName} text-white ${extraClassName}
`} `}
allowFontScaling={false} allowFontScaling={false}
@@ -41,11 +41,15 @@ export function Input(props: InputProps) {
) : ( ) : (
<TextInput <TextInput
ref={inputRef} ref={inputRef}
className='p-4 rounded-xl bg-neutral-900' className={`p-3 rounded-xl bg-neutral-900 ${
isFocused ? "border-2 border-white" : "border-2 border-neutral-800"
} ${extraClassName}`}
allowFontScaling={false} allowFontScaling={false}
style={[{ color: "white" }, style]} style={[{ color: "white" }, style]}
placeholderTextColor={"#9CA3AF"} placeholderTextColor={"#9CA3AF"}
clearButtonMode='while-editing' clearButtonMode='while-editing'
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
{...otherProps} {...otherProps}
/> />
); );

View File

@@ -10,6 +10,7 @@ import {
Permission, Permission,
} from "@/utils/jellyseerr/server/lib/permissions"; } from "@/utils/jellyseerr/server/lib/permissions";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type { import type {
MovieResult, MovieResult,
TvResult, TvResult,
@@ -17,7 +18,7 @@ import type {
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
result?: MovieResult | TvResult | MovieDetails | TvDetails; result?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast;
mediaTitle: string; mediaTitle: string;
releaseYear: number; releaseYear: number;
canRequest: boolean; canRequest: boolean;

View File

@@ -1,8 +1,5 @@
import { useActionSheet } from "@expo/react-native-action-sheet"; import { useActionSheet } from "@expo/react-native-action-sheet";
import type { import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
BaseItemDto,
BaseItemPerson,
} from "@jellyfin/sdk/lib/generated-client/models";
import { useRouter, useSegments } from "expo-router"; import { useRouter, useSegments } from "expo-router";
import { type PropsWithChildren, useCallback } from "react"; import { type PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native"; import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
@@ -14,10 +11,7 @@ interface Props extends TouchableOpacityProps {
isOffline?: boolean; isOffline?: boolean;
} }
export const itemRouter = ( export const itemRouter = (item: BaseItemDto, from: string) => {
item: BaseItemDto | BaseItemPerson,
from: string,
) => {
if ("CollectionType" in item && item.CollectionType === "livetv") { if ("CollectionType" in item && item.CollectionType === "livetv") {
return `/(auth)/(tabs)/${from}/livetv`; return `/(auth)/(tabs)/${from}/livetv`;
} }
@@ -26,8 +20,8 @@ export const itemRouter = (
return `/(auth)/(tabs)/${from}/series/${item.Id}`; return `/(auth)/(tabs)/${from}/series/${item.Id}`;
} }
if (item.Type === "Person" || item.Type === "Actor") { if (item.Type === "Person") {
return `/(auth)/(tabs)/${from}/actors/${item.Id}`; return `/(auth)/(tabs)/${from}/persons/${item.Id}`;
} }
if (item.Type === "BoxSet") { if (item.Type === "BoxSet") {

View File

@@ -1,27 +1,9 @@
import { Ionicons } from "@expo/vector-icons";
import { useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useRouter } from "expo-router";
import { t } from "i18next"; import { t } from "i18next";
import { useMemo } from "react"; import { View, type ViewProps } from "react-native";
import {
ActivityIndicator,
TouchableOpacity,
type TouchableOpacityProps,
View,
type ViewProps,
} from "react-native";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { JobStatus } from "@/providers/Downloads/types"; import { JobStatus } from "@/providers/Downloads/types";
import { storage } from "@/utils/mmkv"; import { DownloadCard } from "./DownloadCard";
import { formatTimeString } from "@/utils/time";
import { Button } from "../Button";
const bytesToMB = (bytes: number) => {
return bytes / 1024 / 1024;
};
interface ActiveDownloadsProps extends ViewProps {} interface ActiveDownloadsProps extends ViewProps {}
@@ -52,163 +34,3 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
</View> </View>
); );
} }
interface DownloadCardProps extends TouchableOpacityProps {
process: JobStatus;
}
const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
const { startDownload, pauseDownload, resumeDownload, removeProcess } =
useDownload();
const router = useRouter();
const queryClient = useQueryClient();
const handlePause = async (id: string) => {
try {
await pauseDownload(id);
toast.success(t("home.downloads.toasts.download_paused"));
} catch (error) {
console.error("Error pausing download:", error);
toast.error(t("home.downloads.toasts.could_not_pause_download"));
}
};
const handleResume = async (id: string) => {
try {
await resumeDownload(id);
toast.success(t("home.downloads.toasts.download_resumed"));
} catch (error) {
console.error("Error resuming download:", error);
toast.error(t("home.downloads.toasts.could_not_resume_download"));
}
};
const handleDelete = async (id: string) => {
try {
await removeProcess(id);
toast.success(t("home.downloads.toasts.download_deleted"));
queryClient.invalidateQueries({ queryKey: ["downloads"] });
} catch (error) {
console.error("Error deleting download:", error);
toast.error(t("home.downloads.toasts.could_not_delete_download"));
}
};
const eta = (p: JobStatus) => {
if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
if (bytesRemaining <= 0) return null;
const secondsRemaining = bytesRemaining / p.speed;
return formatTimeString(secondsRemaining, "s");
};
const base64Image = useMemo(() => {
return storage.getString(process.item.Id!);
}, []);
return (
<TouchableOpacity
onPress={() => router.push(`/(auth)/items/page?id=${process.item.Id}`)}
className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden'
{...props}
>
{process.status === "downloading" && (
<View
className={`
bg-purple-600 h-1 absolute bottom-0 left-0
`}
style={{
width: process.progress
? `${Math.max(5, process.progress)}%`
: "5%",
}}
/>
)}
<View className='px-3 py-1.5 flex flex-col w-full'>
<View className='flex flex-row items-center w-full'>
{base64Image && (
<View className='w-14 aspect-[10/15] rounded-lg overflow-hidden mr-4'>
<Image
source={{
uri: `data:image/jpeg;base64,${base64Image}`,
}}
style={{
width: "100%",
height: "100%",
}}
contentFit='cover'
/>
</View>
)}
<View className='shrink mb-1'>
<Text className='text-xs opacity-50'>{process.item.Type}</Text>
<Text className='font-semibold shrink'>{process.item.Name}</Text>
<Text className='text-xs opacity-50'>
{process.item.ProductionYear}
</Text>
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
{process.progress === 0 ? (
<ActivityIndicator size={"small"} color={"white"} />
) : (
<Text className='text-xs'>{process.progress.toFixed(0)}%</Text>
)}
{process.speed && process.speed > 0 && (
<Text className='text-xs'>
{bytesToMB(process.speed).toFixed(2)} MB/s
</Text>
)}
{eta(process) && (
<Text className='text-xs'>
{t("home.downloads.eta", { eta: eta(process) })}
</Text>
)}
</View>
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
<Text className='text-xs capitalize'>{process.status}</Text>
</View>
</View>
<View className='ml-auto flex flex-row items-center space-x-2'>
{process.status === "downloading" && (
<TouchableOpacity
onPress={() => handlePause(process.id)}
className='p-2 rounded-full bg-yellow-600'
>
<Ionicons name='pause' size={20} color='white' />
</TouchableOpacity>
)}
{process.status === "paused" && (
<TouchableOpacity
onPress={() => handleResume(process.id)}
className='p-2 rounded-full bg-green-600'
>
<Ionicons name='play' size={20} color='white' />
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => handleDelete(process.id)}
className='p-2 rounded-full bg-red-600'
>
<Ionicons name='close' size={20} color='white' />
</TouchableOpacity>
</View>
</View>
{process.status === "completed" && (
<View className='flex flex-row mt-4 space-x-4'>
<Button
onPress={() => {
startDownload(process);
}}
className='w-full'
>
Download now
</Button>
</View>
)}
</View>
</TouchableOpacity>
);
};

View File

@@ -0,0 +1,198 @@
import { Ionicons } from "@expo/vector-icons";
import { useQueryClient } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useRouter } from "expo-router";
import { t } from "i18next";
import { useMemo } from "react";
import {
ActivityIndicator,
TouchableOpacity,
type TouchableOpacityProps,
View,
} from "react-native";
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
import { JobStatus } from "@/providers/Downloads/types";
import { storage } from "@/utils/mmkv";
import { formatTimeString } from "@/utils/time";
import { Button } from "../Button";
const bytesToMB = (bytes: number) => {
return bytes / 1024 / 1024;
};
interface DownloadCardProps extends TouchableOpacityProps {
process: JobStatus;
}
export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
const { startDownload, pauseDownload, resumeDownload, removeProcess } =
useDownload();
const router = useRouter();
const queryClient = useQueryClient();
const handlePause = async (id: string) => {
try {
await pauseDownload(id);
toast.success(t("home.downloads.toasts.download_paused"));
} catch (error) {
console.error("Error pausing download:", error);
toast.error(t("home.downloads.toasts.could_not_pause_download"));
}
};
const handleResume = async (id: string) => {
try {
await resumeDownload(id);
toast.success(t("home.downloads.toasts.download_resumed"));
} catch (error) {
console.error("Error resuming download:", error);
toast.error(t("home.downloads.toasts.could_not_resume_download"));
}
};
const handleDelete = async (id: string) => {
try {
await removeProcess(id);
toast.success(t("home.downloads.toasts.download_deleted"));
queryClient.invalidateQueries({ queryKey: ["downloads"] });
} catch (error) {
console.error("Error deleting download:", error);
toast.error(t("home.downloads.toasts.could_not_delete_download"));
}
};
const eta = (p: JobStatus) => {
if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
if (bytesRemaining <= 0) return null;
const secondsRemaining = bytesRemaining / p.speed;
return formatTimeString(secondsRemaining, "s");
};
const base64Image = useMemo(() => {
return storage.getString(process.item.Id!);
}, []);
// Sanitize progress to ensure it's within valid bounds
const sanitizedProgress = useMemo(() => {
if (
typeof process.progress !== "number" ||
Number.isNaN(process.progress)
) {
return 0;
}
return Math.max(0, Math.min(100, process.progress));
}, [process.progress]);
return (
<TouchableOpacity
onPress={() => router.push(`/(auth)/items/page?id=${process.item.Id}`)}
className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden'
{...props}
>
{process.status === "downloading" && (
<View
className={`
bg-purple-600 h-1 absolute bottom-0 left-0
`}
style={{
width:
sanitizedProgress > 0
? `${Math.max(5, sanitizedProgress)}%`
: "5%",
}}
/>
)}
{/* Action buttons in top right corner */}
<View className='absolute top-2 right-2 flex flex-row items-center space-x-2 z-10'>
{process.status === "downloading" && (
<TouchableOpacity
onPress={() => handlePause(process.id)}
className='p-1'
>
<Ionicons name='pause' size={20} color='white' />
</TouchableOpacity>
)}
{process.status === "paused" && (
<TouchableOpacity
onPress={() => handleResume(process.id)}
className='p-1'
>
<Ionicons name='play' size={20} color='white' />
</TouchableOpacity>
)}
<TouchableOpacity
onPress={() => handleDelete(process.id)}
className='p-1'
>
<Ionicons name='close' size={20} color='red' />
</TouchableOpacity>
</View>
<View className='px-3 py-1.5 flex flex-col w-full'>
<View className='flex flex-row items-center w-full'>
{base64Image && (
<View className='w-14 aspect-[10/15] rounded-lg overflow-hidden mr-4'>
<Image
source={{
uri: `data:image/jpeg;base64,${base64Image}`,
}}
style={{
width: "100%",
height: "100%",
}}
contentFit='cover'
/>
</View>
)}
<View className='shrink mb-1 flex-1'>
<Text className='text-xs opacity-50'>{process.item.Type}</Text>
<Text className='font-semibold shrink'>{process.item.Name}</Text>
<Text className='text-xs opacity-50'>
{process.item.ProductionYear}
</Text>
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
{sanitizedProgress === 0 ? (
<ActivityIndicator size={"small"} color={"white"} />
) : (
<Text className='text-xs'>{sanitizedProgress.toFixed(0)}%</Text>
)}
{process.speed && process.speed > 0 && (
<Text className='text-xs'>
{bytesToMB(process.speed).toFixed(2)} MB/s
</Text>
)}
{eta(process) && (
<Text className='text-xs'>
{t("home.downloads.eta", { eta: eta(process) })}
</Text>
)}
</View>
<View className='flex flex-row items-center space-x-2 mt-1 text-purple-600'>
<Text className='text-xs capitalize'>{process.status}</Text>
</View>
</View>
</View>
{process.status === "completed" && (
<View className='flex flex-row mt-4 space-x-4'>
<Button
onPress={() => {
startDownload(process);
}}
className='w-full'
>
Download now
</Button>
</View>
)}
</View>
</TouchableOpacity>
);
};

View File

@@ -177,7 +177,7 @@ export const FilterSheet = <T,>({
{showSearch && ( {showSearch && (
<Input <Input
placeholder={t("search.search")} placeholder={t("search.search")}
className='my-2 border-neutral-800 border' extraClassName='my-2 border-neutral-800 border'
value={search} value={search}
onChangeText={(text) => { onChangeText={(text) => {
setSearch(text); setSearch(text);

View File

@@ -26,7 +26,7 @@ import { itemRouter } from "../common/TouchableItemRouter";
interface Props extends ViewProps {} interface Props extends ViewProps {}
export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => { export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
const [settings] = useSettings(null); const { settings } = useSettings();
const ref = React.useRef<ICarouselInstance>(null); const ref = React.useRef<ICarouselInstance>(null);
const progress = useSharedValue<number>(0); const progress = useSharedValue<number>(0);

View File

@@ -11,16 +11,12 @@ import {
useJellyseerr, useJellyseerr,
} from "@/hooks/useJellyseerr"; } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
slide, slide,
...props ...props
}) => { }) => {
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "discover", slide.id], queryKey: ["jellyseerr", "discover", slide.id],
@@ -69,7 +65,9 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
uniqBy( uniqBy(
data?.pages data?.pages
?.filter((p) => p?.results.length) ?.filter((p) => p?.results.length)
.flatMap((p) => p?.results), .flatMap((p) =>
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)),
),
"id", "id",
), ),
[data], [data],
@@ -86,12 +84,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
onEndReached={() => { onEndReached={() => {
if (hasNextPage) fetchNextPage(); if (hasNextPage) fetchNextPage();
}} }}
renderItem={(item) => ( renderItem={(item) => <JellyseerrPoster item={item} key={item?.id} />}
<JellyseerrPoster
item={item as MovieResult | TvResult}
key={item?.id}
/>
)}
/> />
) )
); );

View File

@@ -41,7 +41,7 @@ const icons: Record<CollectionType, IconName> = {
export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => { export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const [settings] = useSettings(null); const { settings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import type { PropsWithChildren, ReactNode } from "react"; import type { PropsWithChildren, ReactNode } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { Platform, TouchableOpacity, View, type ViewProps } from "react-native";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
interface Props extends ViewProps { interface Props extends ViewProps {
@@ -34,7 +34,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
disabled={disabled} disabled={disabled}
onPress={onPress} onPress={onPress}
className={`flex flex-row items-center justify-between bg-neutral-900 h-11 pr-4 pl-4 ${ className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${
disabled ? "opacity-50" : "" disabled ? "opacity-50" : ""
}`} }`}
{...(viewProps as any)} {...(viewProps as any)}
@@ -54,7 +54,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
); );
return ( return (
<View <View
className={`flex flex-row items-center justify-between bg-neutral-900 h-11 pr-4 pl-4 ${ className={`flex flex-row items-center justify-between bg-neutral-900 min-h-[42px] py-2 pr-4 pl-4 ${
disabled ? "opacity-50" : "" disabled ? "opacity-50" : ""
}`} }`}
{...viewProps} {...viewProps}
@@ -106,7 +106,10 @@ const ListItemContent = ({
{title} {title}
</Text> </Text>
{subtitle && ( {subtitle && (
<Text className='text-[#9899A1] text-sm mt-0.5' numberOfLines={2}> <Text
className='text-[#9899A1] text-[12px] mt-0.5'
numberOfLines={2}
>
{subtitle} {subtitle}
</Text> </Text>
)} )}

View File

@@ -12,7 +12,7 @@ import { useAtom } from "jotai";
import { useCallback } from "react"; import { useCallback } from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { InfiniteHorizontalScroll } from "../common/InfiniteHorrizontalScroll"; import { InfiniteHorizontalScroll } from "../common/InfiniteHorizontalScroll";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { TouchableItemRouter } from "../common/TouchableItemRouter"; import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { ItemCardText } from "../ItemCardText"; import { ItemCardText } from "../ItemCardText";

View File

@@ -20,6 +20,7 @@ import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker"; import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type { import type {
MovieResult, MovieResult,
TvResult, TvResult,
@@ -27,7 +28,7 @@ import type {
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
interface Props extends ViewProps { interface Props extends ViewProps {
item?: MovieResult | TvResult | MovieDetails | TvDetails; item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast;
horizontal?: boolean; horizontal?: boolean;
showDownloadInfo?: boolean; showDownloadInfo?: boolean;
mediaRequest?: MediaRequest; mediaRequest?: MediaRequest;

View File

@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { TouchableOpacity, View, type ViewProps } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { HorizontalScroll } from "../common/HorrizontalScroll"; import { HorizontalScroll } from "../common/HorizontalScroll";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { itemRouter } from "../common/TouchableItemRouter"; import { itemRouter } from "../common/TouchableItemRouter";
import Poster from "../posters/Poster"; import Poster from "../posters/Poster";
@@ -27,16 +27,18 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
const from = segments[2]; const from = segments[2];
const destinctPeople = useMemo(() => { const destinctPeople = useMemo(() => {
const people: BaseItemPerson[] = []; const people: Record<string, BaseItemPerson> = {};
item?.People?.forEach((person) => { item?.People?.forEach((person) => {
const existingPerson = people.find((p) => p.Id === person.Id); if (!person.Id) return;
const existingPerson = people[person.Id];
if (existingPerson) { if (existingPerson) {
existingPerson.Role = `${existingPerson.Role}, ${person.Role}`; existingPerson.Role = `${existingPerson.Role}, ${person.Role}`;
} else { } else {
people.push(person); people[person.Id] = person;
} }
}); });
return people; return Object.values(people);
}, [item?.People]); }, [item?.People]);
if (!from) return null; if (!from) return null;
@@ -54,7 +56,13 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
renderItem={(i) => ( renderItem={(i) => (
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
const url = itemRouter(i, from); const url = itemRouter(
{
Id: i.Id,
Type: "Person",
},
from,
);
// @ts-expect-error // @ts-expect-error
router.push(url); router.push(url);
}} }}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { TouchableOpacity, View, type ViewProps } from "react-native"; import { TouchableOpacity, View, type ViewProps } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
import { HorizontalScroll } from "../common/HorrizontalScroll"; import { HorizontalScroll } from "../common/HorizontalScroll";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import Poster from "../posters/Poster"; import Poster from "../posters/Poster";

View File

@@ -11,7 +11,7 @@ import { orderBy } from "lodash";
import type React from "react"; import type React from "react";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { Alert, TouchableOpacity, View } from "react-native"; import { Alert, TouchableOpacity, View } from "react-native";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; import { HorizontalScroll } from "@/components/common/HorizontalScroll";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Tags } from "@/components/GenreTags"; import { Tags } from "@/components/GenreTags";
import { dateOpts } from "@/components/jellyseerr/DetailFacts"; import { dateOpts } from "@/components/jellyseerr/DetailFacts";

View File

@@ -12,7 +12,7 @@ import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { import {
HorizontalScroll, HorizontalScroll,
type HorizontalScrollRef, type HorizontalScrollRef,
} from "../common/HorrizontalScroll"; } from "../common/HorizontalScroll";
import { ItemCardText } from "../ItemCardText"; import { ItemCardText } from "../ItemCardText";
interface Props extends ViewProps { interface Props extends ViewProps {
@@ -42,11 +42,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
return item?.SeasonId; return item?.SeasonId;
}, [item]); }, [item]);
const { const { data: episodes, isPending } = useQuery({
data: episodes,
isLoading,
isFetching,
} = useQuery({
queryKey: ["episodes", seasonId, isOffline], queryKey: ["episodes", seasonId, isOffline],
queryFn: async () => { queryFn: async () => {
if (isOffline) { if (isOffline) {
@@ -132,7 +128,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
ref={scrollRef} ref={scrollRef}
data={episodes} data={episodes}
extraData={item} extraData={item}
loading={loading || isLoading || isFetching} loading={loading || isPending}
renderItem={(_item, _idx) => ( renderItem={(_item, _idx) => (
<TouchableOpacity <TouchableOpacity
key={_item.Id} key={_item.Id}

View File

@@ -74,7 +74,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
return season.Id!; return season.Id!;
}, [seasons, seasonIndex]); }, [seasons, seasonIndex]);
const { data: episodes, isFetching } = useQuery({ const { data: episodes, isPending } = useQuery({
queryKey: ["episodes", item.Id, selectedSeasonId], queryKey: ["episodes", item.Id, selectedSeasonId],
queryFn: async () => { queryFn: async () => {
if (!api || !user?.Id || !item.Id || !selectedSeasonId) { if (!api || !user?.Id || !item.Id || !selectedSeasonId) {
@@ -98,10 +98,6 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
return res.data.Items; return res.data.Items;
}, },
select: (data) =>
[...(data || [])].sort(
(a, b) => (a.IndexNumber ?? 0) - (b.IndexNumber ?? 0),
),
enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId, enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId,
}); });
@@ -169,7 +165,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
) : null} ) : null}
</View> </View>
<View className='px-4 flex flex-col mt-4'> <View className='px-4 flex flex-col mt-4'>
{isFetching ? ( {isPending ? (
<View <View
style={{ style={{
minHeight: 144 * nrOfEpisodes, minHeight: 144 * nrOfEpisodes,

View File

@@ -12,7 +12,7 @@ interface Props extends ViewProps {}
export const AppLanguageSelector: React.FC<Props> = () => { export const AppLanguageSelector: React.FC<Props> = () => {
const isTv = Platform.isTV; const isTv = Platform.isTV;
const [settings, updateSettings] = useSettings(null); const { settings, updateSettings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
if (isTv) return null; if (isTv) return null;

View File

@@ -17,7 +17,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
const isTv = Platform.isTV; const isTv = Platform.isTV;
const media = useMedia(); const media = useMedia();
const [_, __, pluginSettings] = useSettings(null); const { pluginSettings } = useSettings();
const { settings, updateSettings } = media; const { settings, updateSettings } = media;
const cultures = media.cultures; const cultures = media.cultures;
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -4,7 +4,7 @@ import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
export const ChromecastSettings: React.FC = ({ ...props }) => { export const ChromecastSettings: React.FC = ({ ...props }) => {
const [settings, updateSettings] = useSettings(null); const { settings, updateSettings } = useSettings();
return ( return (
<View {...props}> <View {...props}>
<ListGroup title={"Chromecast"}> <ListGroup title={"Chromecast"}>

View File

@@ -7,7 +7,7 @@ import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
export const Dashboard = () => { export const Dashboard = () => {
const [settings, _updateSettings] = useSettings(null); const { settings } = useSettings();
const { sessions = [] } = useSessions({} as useSessionsProps); const { sessions = [] } = useSessions({} as useSessionsProps);
const router = useRouter(); const router = useRouter();

View File

@@ -7,7 +7,7 @@ import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
export default function DownloadSettings({ ...props }) { export default function DownloadSettings({ ...props }) {
const [settings, updateSettings, pluginSettings] = useSettings(null); const { settings, updateSettings, pluginSettings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();
const allDisabled = useMemo( const allDisabled = useMemo(

View File

@@ -13,7 +13,7 @@ interface Props extends ViewProps {}
export const GestureControls: React.FC<Props> = ({ ...props }) => { export const GestureControls: React.FC<Props> = ({ ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [settings, updateSettings, pluginSettings] = useSettings(null); const { settings, updateSettings, pluginSettings } = useSettings();
const disabled = useMemo( const disabled = useMemo(
() => () =>
@@ -36,6 +36,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
"home.settings.gesture_controls.horizontal_swipe_skip_description", "home.settings.gesture_controls.horizontal_swipe_skip_description",
)} )}
disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked} disabled={pluginSettings?.enableHorizontalSwipeSkip?.locked}
style={{ minHeight: 72, paddingTop: 12, paddingBottom: 12 }}
> >
<Switch <Switch
value={settings.enableHorizontalSwipeSkip} value={settings.enableHorizontalSwipeSkip}
@@ -52,6 +53,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
"home.settings.gesture_controls.left_side_brightness_description", "home.settings.gesture_controls.left_side_brightness_description",
)} )}
disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked} disabled={pluginSettings?.enableLeftSideBrightnessSwipe?.locked}
style={{ minHeight: 72, paddingTop: 12, paddingBottom: 12 }}
> >
<Switch <Switch
value={settings.enableLeftSideBrightnessSwipe} value={settings.enableLeftSideBrightnessSwipe}
@@ -68,6 +70,7 @@ export const GestureControls: React.FC<Props> = ({ ...props }) => {
"home.settings.gesture_controls.right_side_volume_description", "home.settings.gesture_controls.right_side_volume_description",
)} )}
disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked} disabled={pluginSettings?.enableRightSideVolumeSwipe?.locked}
style={{ minHeight: 72, paddingTop: 12, paddingBottom: 12 }}
> >
<Switch <Switch
value={settings.enableRightSideVolumeSwipe} value={settings.enableRightSideVolumeSwipe}

View File

@@ -11,7 +11,6 @@ import {
getUserLibraryApi, getUserLibraryApi,
getUserViewsApi, getUserViewsApi,
} from "@jellyfin/sdk/lib/utils/api"; } from "@jellyfin/sdk/lib/utils/api";
import NetInfo from "@react-native-community/netinfo";
import { type QueryFunction, useQuery } from "@tanstack/react-query"; import { type QueryFunction, useQuery } from "@tanstack/react-query";
import { useNavigation, useRouter, useSegments } from "expo-router"; import { useNavigation, useRouter, useSegments } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
@@ -33,6 +32,7 @@ import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionLi
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { MediaListSection } from "@/components/medialists/MediaListSection"; import { MediaListSection } from "@/components/medialists/MediaListSection";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
@@ -64,16 +64,7 @@ export const HomeIndex = () => {
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [ const { settings, refreshStreamyfinPluginSettings } = useSettings();
settings,
_updateSettings,
_pluginSettings,
_setPluginSettings,
refreshStreamyfinPluginSettings,
] = useSettings(null);
const [isConnected, setIsConnected] = useState<boolean | null>(null);
const [loadingRetry, setLoadingRetry] = useState(false);
const navigation = useNavigation(); const navigation = useNavigation();
@@ -83,6 +74,7 @@ export const HomeIndex = () => {
const { getDownloadedItems, cleanCacheDirectory } = useDownload(); const { getDownloadedItems, cleanCacheDirectory } = useDownload();
const prevIsConnected = useRef<boolean | null>(false); const prevIsConnected = useRef<boolean | null>(false);
const { isConnected, loading: retryLoading, retryCheck } = useNetworkStatus();
const invalidateCache = useInvalidatePlaybackProgressCache(); const invalidateCache = useInvalidatePlaybackProgressCache();
useEffect(() => { useEffect(() => {
// Only invalidate cache when transitioning from offline to online // Only invalidate cache when transitioning from offline to online
@@ -137,29 +129,6 @@ export const HomeIndex = () => {
}; };
}, [segments]); }, [segments]);
const checkConnection = useCallback(async () => {
setLoadingRetry(true);
const state = await NetInfo.fetch();
setIsConnected(state.isConnected);
setLoadingRetry(false);
}, []);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
if (state.isConnected === false || state.isInternetReachable === false)
setIsConnected(false);
else setIsConnected(true);
});
NetInfo.fetch().then((state) => {
setIsConnected(state.isConnected);
});
return () => {
unsubscribe();
};
}, []);
const { const {
data, data,
isError: e1, isError: e1,
@@ -341,7 +310,7 @@ export const HomeIndex = () => {
for (const [index, section] of settings.home.sections.entries()) { for (const [index, section] of settings.home.sections.entries()) {
const id = section.items?.title || `section-${index}`; const id = section.items?.title || `section-${index}`;
ss.push({ ss.push({
title: id, title: t(`${id}`),
queryKey: ["home", id], queryKey: ["home", id],
queryFn: async () => { queryFn: async () => {
if (section.items) { if (section.items) {
@@ -397,30 +366,30 @@ export const HomeIndex = () => {
{t("home.no_internet_message")} {t("home.no_internet_message")}
</Text> </Text>
<View className='mt-4'> <View className='mt-4'>
<Button {!Platform.isTV && (
color='purple' <Button
onPress={() => router.push("/(auth)/downloads")} color='purple'
justify='center' onPress={() => router.push("/(auth)/downloads")}
iconRight={ justify='center'
<Ionicons name='arrow-forward' size={20} color='white' /> iconRight={
} <Ionicons name='arrow-forward' size={20} color='white' />
> }
{t("home.go_to_downloads")} >
</Button> {t("home.go_to_downloads")}
</Button>
)}
<Button <Button
color='black' color='black'
onPress={() => { onPress={retryCheck}
checkConnection();
}}
justify='center' justify='center'
className='mt-2' className='mt-2'
iconRight={ iconRight={
loadingRetry ? null : ( retryLoading ? null : (
<Ionicons name='refresh' size={20} color='white' /> <Ionicons name='refresh' size={20} color='white' />
) )
} }
> >
{loadingRetry ? ( {retryLoading ? (
<ActivityIndicator size={"small"} color={"white"} /> <ActivityIndicator size={"small"} color={"white"} />
) : ( ) : (
"Retry" "Retry"

View File

@@ -4,6 +4,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { PasswordInput } from "@/components/PasswordInput";
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
import { userAtom } from "@/providers/JellyfinProvider"; import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -20,7 +21,7 @@ export const JellyseerrSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const [settings, updateSettings, _pluginSettings] = useSettings(null); const { settings, updateSettings } = useSettings();
const [jellyseerrPassword, setJellyseerrPassword] = useState< const [jellyseerrPassword, setJellyseerrPassword] = useState<
string | undefined string | undefined
@@ -30,6 +31,9 @@ export const JellyseerrSettings = () => {
string | undefined string | undefined
>(settings?.jellyseerrServerUrl || undefined); >(settings?.jellyseerrServerUrl || undefined);
const [showJellyseerrPassword, setShowJellyseerrPassword] =
useState<boolean>(false);
const loginToJellyseerrMutation = useMutation({ const loginToJellyseerrMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!jellyseerrServerUrl && !settings?.jellyseerrServerUrl) if (!jellyseerrServerUrl && !settings?.jellyseerrServerUrl)
@@ -127,7 +131,7 @@ export const JellyseerrSettings = () => {
</Text> </Text>
</View> </View>
<Input <Input
className='border border-neutral-800 mb-2' extraClassName='border border-neutral-800 mb-2'
placeholder={t( placeholder={t(
"home.settings.plugins.jellyseerr.server_url_placeholder", "home.settings.plugins.jellyseerr.server_url_placeholder",
)} )}
@@ -146,23 +150,20 @@ export const JellyseerrSettings = () => {
<Text className='font-bold mb-2'> <Text className='font-bold mb-2'>
{t("home.settings.plugins.jellyseerr.password")} {t("home.settings.plugins.jellyseerr.password")}
</Text> </Text>
<Input <View className='relative'>
className='border border-neutral-800' <PasswordInput
autoFocus={true} value={jellyseerrPassword}
focusable={true} onChangeText={setJellyseerrPassword}
placeholder={t( placeholder={t(
"home.settings.plugins.jellyseerr.password_placeholder", "home.settings.plugins.jellyseerr.password_placeholder",
{ username: user?.Name }, { username: user?.Name },
)} )}
value={jellyseerrPassword} showPassword={showJellyseerrPassword}
keyboardType='default' onShowPasswordChange={setShowJellyseerrPassword}
secureTextEntry={true} layout='mobile'
returnKeyType='done' topOffset={11}
autoCapitalize='none' />
textContentType='password' </View>
onChangeText={setJellyseerrPassword}
editable={!loginToJellyseerrMutation.isPending}
/>
<Button <Button
loading={loginToJellyseerrMutation.isPending} loading={loginToJellyseerrMutation.isPending}
disabled={loginToJellyseerrMutation.isPending} disabled={loginToJellyseerrMutation.isPending}

View File

@@ -28,7 +28,7 @@ export const useMedia = () => {
}; };
export const MediaProvider = ({ children }: { children: ReactNode }) => { export const MediaProvider = ({ children }: { children: ReactNode }) => {
const [settings, updateSettings] = useSettings(null); const { settings, updateSettings } = useSettings();
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const queryClient = useQueryClient(); const queryClient = useQueryClient();

View File

@@ -13,7 +13,7 @@ interface Props extends ViewProps {}
export const MediaToggles: React.FC<Props> = ({ ...props }) => { export const MediaToggles: React.FC<Props> = ({ ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [settings, updateSettings, pluginSettings] = useSettings(null); const { settings, updateSettings, pluginSettings } = useSettings();
const disabled = useMemo( const disabled = useMemo(
() => () =>

View File

@@ -23,7 +23,7 @@ import { ListItem } from "../list/ListItem";
export const OtherSettings: React.FC = () => { export const OtherSettings: React.FC = () => {
const router = useRouter(); const router = useRouter();
const [settings, updateSettings, pluginSettings] = useSettings(null); const { settings, updateSettings, pluginSettings } = useSettings();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -6,7 +6,7 @@ import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
export const PluginSettings = () => { export const PluginSettings = () => {
const [settings, _updateSettings] = useSettings(null); const { settings } = useSettings();
const router = useRouter(); const router = useRouter();

View File

@@ -20,7 +20,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
const isTv = Platform.isTV; const isTv = Platform.isTV;
const media = useMedia(); const media = useMedia();
const [_, __, pluginSettings] = useSettings(null); const { pluginSettings } = useSettings();
const { settings, updateSettings } = media; const { settings, updateSettings } = media;
const cultures = media.cultures; const cultures = media.cultures;
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -17,7 +17,7 @@ export const commonScreenOptions: ICommonScreenOptions = {
headerLeft: () => <HeaderBackButton />, headerLeft: () => <HeaderBackButton />,
}; };
const routes = ["actors/[actorId]", "items/page", "series/[id]"]; const routes = ["persons/[personId]", "items/page", "series/[id]"];
export const nestedTabPageScreenOptions: Record<string, ICommonScreenOptions> = export const nestedTabPageScreenOptions: Record<string, ICommonScreenOptions> =
Object.fromEntries(routes.map((route) => [route, commonScreenOptions])); Object.fromEntries(routes.map((route) => [route, commonScreenOptions]));

View File

@@ -88,7 +88,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
trickplayInfo, trickplayInfo,
time, time,
}) => { }) => {
const [settings] = useSettings(null); const { settings } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
return ( return (

View File

@@ -30,7 +30,7 @@ export const CenterControls: FC<CenterControlsProps> = ({
handleSkipBackward, handleSkipBackward,
handleSkipForward, handleSkipForward,
}) => { }) => {
const [settings] = useSettings(null); const { settings } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
return ( return (

View File

@@ -16,7 +16,7 @@ export interface ContinueWatchingOverlayProps {
const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({ const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
goToNextItem, goToNextItem,
}) => { }) => {
const [settings] = useSettings(null); const { settings } = useSettings();
const router = useRouter(); const router = useRouter();
return settings.autoPlayEpisodeCount >= return settings.autoPlayEpisodeCount >=

View File

@@ -116,7 +116,7 @@ export const Controls: FC<Props> = ({
api = null, api = null,
downloadedFiles = undefined, downloadedFiles = undefined,
}) => { }) => {
const [settings, updateSettings] = useSettings(api); const { settings, updateSettings } = useSettings();
const router = useRouter(); const router = useRouter();
const lightHapticFeedback = useHaptic("light"); const lightHapticFeedback = useHaptic("light");

View File

@@ -11,7 +11,7 @@ import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { import {
HorizontalScroll, HorizontalScroll,
type HorizontalScrollRef, type HorizontalScrollRef,
} from "@/components/common/HorrizontalScroll"; } from "@/components/common/HorizontalScroll";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { import {

View File

@@ -31,7 +31,7 @@ export const GestureOverlay = ({
onSkipForward, onSkipForward,
onSkipBackward, onSkipBackward,
}: Props) => { }: Props) => {
const [settings] = useSettings(null); const { settings } = useSettings();
const lightHaptic = useHaptic("light"); const lightHaptic = useHaptic("light");
const [feedback, setFeedback] = useState<FeedbackState>({ const [feedback, setFeedback] = useState<FeedbackState>({

View File

@@ -70,7 +70,7 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
setVideoAspectRatio, setVideoAspectRatio,
setVideoScaleFactor, setVideoScaleFactor,
}) => { }) => {
const [settings] = useSettings(null); const { settings } = useSettings();
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { width: screenWidth } = useWindowDimensions(); const { width: screenWidth } = useWindowDimensions();

View File

@@ -20,7 +20,7 @@ export function useVideoNavigation({
seek, seek,
play, play,
}: UseVideoNavigationProps) { }: UseVideoNavigationProps) {
const [settings] = useSettings(null); const { settings } = useSettings();
const lightHapticFeedback = useHaptic("light"); const lightHapticFeedback = useHaptic("light");
const wasPlayingRef = useRef(false); const wasPlayingRef = useRef(false);

View File

@@ -26,13 +26,6 @@
"EXPO_PUBLIC_WRITE_DEBUG": "1" "EXPO_PUBLIC_WRITE_DEBUG": "1"
} }
}, },
"preview": {
"environment": "development",
"distribution": "internal",
"env": {
"EXPO_PUBLIC_WRITE_DEBUG": "1"
}
},
"development-simulator": { "development-simulator": {
"environment": "development", "environment": "development",
"developmentClient": true, "developmentClient": true,
@@ -44,16 +37,22 @@
"EXPO_PUBLIC_WRITE_DEBUG": "1" "EXPO_PUBLIC_WRITE_DEBUG": "1"
} }
}, },
"preview": {
"distribution": "internal",
"env": {
"EXPO_PUBLIC_WRITE_DEBUG": "1"
}
},
"production": { "production": {
"environment": "production", "environment": "production",
"channel": "0.35.0", "channel": "0.35.1",
"android": { "android": {
"image": "latest" "image": "latest"
} }
}, },
"production-apk": { "production-apk": {
"environment": "production", "environment": "production",
"channel": "0.35.0", "channel": "0.35.1",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"
@@ -61,7 +60,7 @@
}, },
"production-apk-tv": { "production-apk-tv": {
"environment": "production", "environment": "production",
"channel": "0.35.0", "channel": "0.35.1",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"

View File

@@ -14,7 +14,7 @@ export type HapticFeedbackType =
| "error"; | "error";
export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
const [settings] = useSettings(null); const { settings } = useSettings();
const isTv = Platform.isTV; const isTv = Platform.isTV;
const isDisabled = const isDisabled =
isTv || isTv ||

View File

@@ -14,7 +14,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { t } from "i18next"; import { t } from "i18next";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { defaultValues, Settings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
import { import {
IssueStatus, IssueStatus,
@@ -40,6 +40,7 @@ import type { UserResultsResponse } from "@/utils/jellyseerr/server/interfaces/a
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { import type {
CombinedCredit, CombinedCredit,
PersonCreditCast,
PersonDetails, PersonDetails,
} from "@/utils/jellyseerr/server/models/Person"; } from "@/utils/jellyseerr/server/models/Person";
import type { import type {
@@ -416,10 +417,8 @@ export class JellyseerrApi {
const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER)); const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER));
export const useJellyseerr = ( export const useJellyseerr = () => {
settings: Settings = defaultValues, const { settings, updateSettings } = useSettings();
updateSettings: (update: Partial<Settings>) => void = () => {},
) => {
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom); const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -468,49 +467,47 @@ export const useJellyseerr = (
[jellyseerrApi], [jellyseerrApi],
); );
const isJellyseerrResult = ( const isJellyseerrMovieOrTvResult = (
items: any | null | undefined, items: any | null | undefined,
): items is Results => { ): items is MovieResult | TvResult => {
return ( return (
items && items &&
Object.hasOwn(items, "mediaType") && Object.hasOwn(items, "mediaType") &&
Object.values(MediaType).includes(items.mediaType as MediaType) (items.mediaType === MediaType.MOVIE || items.mediaType === MediaType.TV)
); );
}; };
const getTitle = ( const getTitle = (
item?: TvResult | TvDetails | MovieResult | MovieDetails, item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
) => { ) => {
return isJellyseerrResult(item) return isJellyseerrMovieOrTvResult(item)
? item.mediaType === MediaType.MOVIE ? item.mediaType === MediaType.MOVIE
? item?.title ? item?.title
: item?.name : item?.name
: item?.mediaInfo.mediaType === MediaType.MOVIE : item?.mediaInfo?.mediaType === MediaType.MOVIE
? (item as MovieDetails)?.title ? (item as MovieDetails)?.title
: (item as TvDetails)?.name; : (item as TvDetails)?.name;
}; };
const getYear = ( const getYear = (
item?: TvResult | TvDetails | MovieResult | MovieDetails, item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
) => { ) => {
return new Date( return new Date(
(isJellyseerrResult(item) (isJellyseerrMovieOrTvResult(item)
? item.mediaType === MediaType.MOVIE ? item.mediaType === MediaType.MOVIE
? item?.releaseDate ? item?.releaseDate
: item?.firstAirDate : item?.firstAirDate
: item?.mediaInfo.mediaType === MediaType.MOVIE : item?.mediaInfo?.mediaType === MediaType.MOVIE
? (item as MovieDetails)?.releaseDate ? (item as MovieDetails)?.releaseDate
: (item as TvDetails)?.firstAirDate) || "", : (item as TvDetails)?.firstAirDate) || "",
)?.getFullYear?.(); )?.getFullYear?.();
}; };
const getMediaType = ( const getMediaType = (
item?: TvResult | TvDetails | MovieResult | MovieDetails, item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
): MediaType => { ): MediaType => {
return isJellyseerrResult(item) return isJellyseerrMovieOrTvResult(item)
? item.mediaType === "movie" ? (item.mediaType as MediaType)
? MediaType.MOVIE
: MediaType.TV
: item?.mediaInfo?.mediaType; : item?.mediaInfo?.mediaType;
}; };
@@ -528,7 +525,7 @@ export const useJellyseerr = (
jellyseerrUser, jellyseerrUser,
setJellyseerrUser, setJellyseerrUser,
clearAllJellyseerData, clearAllJellyseerData,
isJellyseerrResult, isJellyseerrMovieOrTvResult,
getTitle, getTitle,
getYear, getYear,
getMediaType, getMediaType,

30
hooks/useNetworkStatus.ts Normal file
View File

@@ -0,0 +1,30 @@
import NetInfo from "@react-native-community/netinfo";
import { useCallback, useEffect, useState } from "react";
export function useNetworkStatus() {
const [isConnected, setIsConnected] = useState(false);
const [loading, setLoading] = useState(false);
// Manual check (optional)
const retryCheck = useCallback(async () => {
setLoading(true);
const state = await NetInfo.fetch();
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
setLoading(false);
}, []);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
});
// Initial state
NetInfo.fetch().then((state) => {
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
});
return () => unsubscribe();
}, []);
return { isConnected, loading, retryCheck };
}

View File

@@ -1,12 +1,12 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useNetInfo } from "@react-native-community/netinfo";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useMemo } from "react"; import { useMemo } from "react";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { DownloadedItem } from "@/providers/Downloads/types"; import { DownloadedItem } from "@/providers/Downloads/types";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useNetworkStatus } from "./useNetworkStatus";
interface PlaybackManagerProps { interface PlaybackManagerProps {
item?: BaseItemDto | null; item?: BaseItemDto | null;
@@ -65,12 +65,12 @@ export const usePlaybackManager = ({
}: PlaybackManagerProps = {}) => { }: PlaybackManagerProps = {}) => {
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
const netInfo = useNetInfo(); const { isConnected } = useNetworkStatus();
const { getDownloadedItemById, updateDownloadedItem, getDownloadedItems } = const { getDownloadedItemById, updateDownloadedItem, getDownloadedItems } =
useDownload(); useDownload();
/** Whether the device is online. actually it's connected to the internet. */ /** Whether the device is online. actually it's connected to the internet. */
const isOnline = useMemo(() => netInfo.isConnected, [netInfo.isConnected]); const isOnline = isConnected;
// Adjacent episodes logic // Adjacent episodes logic
const { data: adjacentItems } = useQuery({ const { data: adjacentItems } = useQuery({

View File

@@ -1,8 +1,8 @@
import { getItemsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { getItemsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useNetInfo } from "@react-native-community/netinfo";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "../providers/JellyfinProvider"; import { apiAtom, userAtom } from "../providers/JellyfinProvider";
import { useNetworkStatus } from "./useNetworkStatus";
/** /**
* This hook is used to sync the playback state of a downloaded item with the server * This hook is used to sync the playback state of a downloaded item with the server
@@ -11,8 +11,8 @@ import { apiAtom, userAtom } from "../providers/JellyfinProvider";
export const useTwoWaySync = () => { export const useTwoWaySync = () => {
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom); const user = useAtomValue(userAtom);
const netInfo = useNetInfo();
const { getDownloadedItemById, updateDownloadedItem } = useDownload(); const { getDownloadedItemById, updateDownloadedItem } = useDownload();
const { isConnected } = useNetworkStatus();
/** /**
* Syncs the playback state of an offline item with the server. * Syncs the playback state of an offline item with the server.
@@ -21,7 +21,7 @@ export const useTwoWaySync = () => {
* @returns A Promise<boolean> indicating whether a server update was made (true) or not (false). * @returns A Promise<boolean> indicating whether a server update was made (true) or not (false).
*/ */
const syncPlaybackState = async (itemId: string): Promise<boolean> => { const syncPlaybackState = async (itemId: string): Promise<boolean> => {
if (!api || !user || !netInfo.isConnected) { if (!api || !user || !isConnected) {
// Cannot sync if offline or not logged in // Cannot sync if offline or not logged in
return false; return false;
} }

View File

@@ -25,7 +25,7 @@
"@bottom-tabs/react-navigation": "^0.9.2", "@bottom-tabs/react-navigation": "^0.9.2",
"@expo/metro-runtime": "~5.0.4", "@expo/metro-runtime": "~5.0.4",
"@expo/react-native-action-sheet": "^4.1.1", "@expo/react-native-action-sheet": "^4.1.1",
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.1.0", "@gorhom/bottom-sheet": "^5.1.0",
"@jellyfin/sdk": "^0.11.0", "@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.6", "@kesha-antonov/react-native-background-downloader": "^3.2.6",
@@ -94,28 +94,28 @@
"react-native-video": "6.14.1", "react-native-video": "6.14.1",
"react-native-volume-manager": "^2.0.8", "react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.20.0", "react-native-web": "^0.20.0",
"sonner-native": "^0.21.0", "sonner-native": "^0.21.1",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"zeego": "^3.0.6", "zeego": "^3.0.6",
"zod": "^4.1.3" "zod": "^4.1.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "7.28.3",
"@biomejs/biome": "^2.2.2", "@biomejs/biome": "2.2.2",
"@react-native-community/cli": "^20.0.0", "@react-native-community/cli": "20.0.1",
"@react-native-tvos/config-tv": "^0.1.1", "@react-native-tvos/config-tv": "0.1.3",
"@types/jest": "^29.5.12", "@types/jest": "29.5.14",
"@types/lodash": "^4.17.15", "@types/lodash": "4.17.20",
"@types/react": "~19.0.10", "@types/react": "~19.0.10",
"@types/react-test-renderer": "^19.0.0", "@types/react-test-renderer": "19.1.0",
"expo-doctor": "^1.17.0", "expo-doctor": "1.17.2",
"cross-env": "^10.0.0", "cross-env": "10.0.0",
"husky": "^9.1.7", "husky": "9.1.7",
"lint-staged": "^16.1.5", "lint-staged": "16.1.6",
"postinstall-postinstall": "^2.1.0", "postinstall": "0.11.2",
"react-test-renderer": "19.1.1", "react-test-renderer": "19.1.1",
"typescript": "~5.8.3" "typescript": "5.8.3"
}, },
"expo": { "expo": {
"install": { "install": {
@@ -123,7 +123,8 @@
"react-native", "react-native",
"@shopify/flash-list", "@shopify/flash-list",
"react-native-reanimated", "react-native-reanimated",
"react-native-pager-view" "react-native-pager-view",
"@expo/vector-icons"
] ]
}, },
"doctor": { "doctor": {
@@ -149,7 +150,7 @@
] ]
}, },
"trustedDependencies": [ "trustedDependencies": [
"postinstall-postinstall", "postinstall",
"unrs-resolver" "unrs-resolver"
] ]
} }

View File

@@ -4,6 +4,7 @@ import type {
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import * as Application from "expo-application"; import * as Application from "expo-application";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import * as Notifications from "expo-notifications";
import { router } from "expo-router"; import { router } from "expo-router";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { throttle } from "lodash"; import { throttle } from "lodash";
@@ -87,9 +88,72 @@ function useDownloadProvider() {
const { saveSeriesPrimaryImage } = useDownloadHelper(); const { saveSeriesPrimaryImage } = useDownloadHelper();
const { saveImage } = useImageStorage(); const { saveImage } = useImageStorage();
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom); const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
const [settings] = useSettings(null); const { settings } = useSettings();
const successHapticFeedback = useHaptic("success"); const successHapticFeedback = useHaptic("success");
// Generate notification content based on item type
const getNotificationContent = useCallback(
(item: BaseItemDto, isSuccess: boolean) => {
if (item.Type === "Episode") {
const season = item.ParentIndexNumber
? String(item.ParentIndexNumber).padStart(2, "0")
: "??";
const episode = item.IndexNumber
? String(item.IndexNumber).padStart(2, "0")
: "??";
const subtitle = `${item.Name} - [S${season}E${episode}] (${item.SeriesName})`;
return {
title: isSuccess ? "Download complete" : "Download failed",
body: subtitle,
};
} else if (item.Type === "Movie") {
const year = item.ProductionYear ? ` (${item.ProductionYear})` : "";
const subtitle = `${item.Name}${year}`;
return {
title: isSuccess ? "Download complete" : "Download failed",
body: subtitle,
};
} else {
// Fallback for other types
return {
title: isSuccess
? t("home.downloads.toasts.download_completed_for_item", {
item: item.Name,
})
: t("home.downloads.toasts.download_failed_for_item", {
item: item.Name,
}),
body: item.Name || "Unknown item",
};
}
},
[t],
);
// Send local notification for download events
const sendDownloadNotification = useCallback(
async (title: string, body: string, data?: Record<string, any>) => {
if (Platform.isTV) return;
try {
await Notifications.scheduleNotificationAsync({
content: {
title,
body,
data,
...(Platform.OS === "android" && { channelId: "downloads" }),
},
trigger: null, // Show immediately
});
} catch (error) {
console.error("Failed to send notification:", error);
}
},
[],
);
/// Cant use the background downloader callback. As its not triggered if size is unknown. /// Cant use the background downloader callback. As its not triggered if size is unknown.
const updateProgress = async () => { const updateProgress = async () => {
const tasks = await BackGroundDownloader.checkForExistingDownloads(); const tasks = await BackGroundDownloader.checkForExistingDownloads();
@@ -418,6 +482,21 @@ function useDownloadProvider() {
} }
await saveDownloadsDatabase(db); await saveDownloadsDatabase(db);
// Send native notification for successful download
const successNotification = getNotificationContent(
process.item,
true,
);
await sendDownloadNotification(
successNotification.title,
successNotification.body,
{
itemId: process.item.Id,
itemName: process.item.Name,
type: "download_completed",
},
);
toast.success( toast.success(
t("home.downloads.toasts.download_completed_for_item", { t("home.downloads.toasts.download_completed_for_item", {
item: process.item.Name, item: process.item.Name,
@@ -425,8 +504,25 @@ function useDownloadProvider() {
); );
removeProcess(process.id); removeProcess(process.id);
}) })
.error((error: any) => { .error(async (error: any) => {
console.error("Download error:", error); console.error("Download error:", error);
// Send native notification for failed download
const failureNotification = getNotificationContent(
process.item,
false,
);
await sendDownloadNotification(
failureNotification.title,
failureNotification.body,
{
itemId: process.item.Id,
itemName: process.item.Name,
type: "download_failed",
error: error?.message || "Unknown error",
},
);
toast.error( toast.error(
t("home.downloads.toasts.download_failed_for_item", { t("home.downloads.toasts.download_failed_for_item", {
item: process.item.Name, item: process.item.Name,
@@ -435,7 +531,7 @@ function useDownloadProvider() {
removeProcess(process.id); removeProcess(process.id);
}); });
}, },
[authHeader], [authHeader, sendDownloadNotification, getNotificationContent],
); );
const manageDownloadQueue = useCallback(() => { const manageDownloadQueue = useCallback(() => {

View File

@@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin( setJellyfin(
() => () =>
new Jellyfin({ new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.35.0" }, clientInfo: { name: "Streamyfin", version: "0.35.1" },
deviceInfo: { deviceInfo: {
name: deviceName, name: deviceName,
id, id,
@@ -79,24 +79,15 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const [user, setUser] = useAtom(userAtom); const [user, setUser] = useAtom(userAtom);
const [isPolling, setIsPolling] = useState<boolean>(false); const [isPolling, setIsPolling] = useState<boolean>(false);
const [secret, setSecret] = useState<string | null>(null); const [secret, setSecret] = useState<string | null>(null);
const [ const { setPluginSettings, refreshStreamyfinPluginSettings } = useSettings();
_settings, const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr();
_updateSettings,
_pluginSettings,
setPluginSettings,
refreshStreamyfinPluginSettings,
] = useSettings(api);
const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(
_settings || {},
_updateSettings,
);
const headers = useMemo(() => { const headers = useMemo(() => {
if (!deviceId) return {}; if (!deviceId) return {};
return { return {
authorization: `MediaBrowser Client="Streamyfin", Device=${ authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS" Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.35.0"`, }, DeviceId="${deviceId}", Version="0.35.1"`,
}; };
}, [deviceId]); }, [deviceId]);

View File

@@ -22,7 +22,10 @@
"there_is_a_server_error": "There is a server error", "there_is_a_server_error": "There is a server error",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?", "an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
"too_old_server_text": "Unsupported jellyfin server discovered", "too_old_server_text": "Unsupported jellyfin server discovered",
"too_old_server_description": "Please update jellyfin to the latest version" "too_old_server_description": "Please update jellyfin to the latest version",
"show_password": "Show password",
"hide_password": "Hide password",
"toggle_password_visibility": "Toggle password visibility"
}, },
"server": { "server": {
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server", "enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",

View File

@@ -3,11 +3,13 @@ import { useJellyseerr } from "@/hooks/useJellyseerr";
import { import {
MediaRequestStatus, MediaRequestStatus,
MediaStatus, MediaStatus,
MediaType,
} from "@/utils/jellyseerr/server/constants/media"; } from "@/utils/jellyseerr/server/constants/media";
import { import {
hasPermission, hasPermission,
Permission, Permission,
} from "@/utils/jellyseerr/server/lib/permissions"; } from "@/utils/jellyseerr/server/lib/permissions";
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type { import type {
MovieResult, MovieResult,
TvResult, TvResult,
@@ -17,7 +19,7 @@ import type { MovieDetails } from "../jellyseerr/server/models/Movie";
import type { TvDetails } from "../jellyseerr/server/models/Tv"; import type { TvDetails } from "../jellyseerr/server/models/Tv";
export const useJellyseerrCanRequest = ( export const useJellyseerrCanRequest = (
item?: MovieResult | TvResult | MovieDetails | TvDetails, item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast,
) => { ) => {
const { jellyseerrUser } = useJellyseerr(); const { jellyseerrUser } = useJellyseerr();
@@ -40,7 +42,7 @@ export const useJellyseerrCanRequest = (
const userHasPermission = hasPermission( const userHasPermission = hasPermission(
[ [
Permission.REQUEST, Permission.REQUEST,
item?.mediaInfo?.mediaType item?.mediaInfo?.mediaType === MediaType.MOVIE
? Permission.REQUEST_MOVIE ? Permission.REQUEST_MOVIE
: Permission.REQUEST_TV, : Permission.REQUEST_TV,
], ],

View File

@@ -56,7 +56,7 @@ export const useJobProcessor = () => {
const [queue, setQueue] = useAtom(queueAtom); const [queue, setQueue] = useAtom(queueAtom);
const [running, setRunning] = useAtom(runningAtom); const [running, setRunning] = useAtom(runningAtom);
const [processes] = useAtom<JobStatus[]>(processesAtom); const [processes] = useAtom<JobStatus[]>(processesAtom);
const [settings] = useSettings(null); const { settings } = useSettings();
useEffect(() => { useEffect(() => {
if ( if (

View File

@@ -1,4 +1,3 @@
import type { Api } from "@jellyfin/sdk";
import { import {
type BaseItemKind, type BaseItemKind,
type CultureDto, type CultureDto,
@@ -7,11 +6,12 @@ import {
type SortOrder, type SortOrder,
SubtitlePlaybackMode, SubtitlePlaybackMode,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import { atom, useAtom } from "jotai"; import { atom, useAtom, useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { BITRATES, type Bitrate } from "@/components/BitrateSelector"; import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom } from "@/providers/JellyfinProvider";
import { writeInfoLog } from "@/utils/log"; import { writeInfoLog } from "@/utils/log";
import { storage } from "../mmkv"; import { storage } from "../mmkv";
@@ -278,7 +278,8 @@ export const pluginSettingsAtom = atom<PluginLockableSettings | undefined>(
loadPluginSettings(), loadPluginSettings(),
); );
export const useSettings = (api: Api | null) => { export const useSettings = () => {
const api = useAtomValue(apiAtom);
const [_settings, setSettings] = useAtom(settingsAtom); const [_settings, setSettings] = useAtom(settingsAtom);
const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom); const [pluginSettings, _setPluginSettings] = useAtom(pluginSettingsAtom);
@@ -302,11 +303,11 @@ export const useSettings = (api: Api | null) => {
return; return;
} }
const settings = await api.getStreamyfinPluginConfig().then( const settings = await api.getStreamyfinPluginConfig().then(
({ data }: any) => { ({ data }) => {
writeInfoLog("Got plugin settings", data?.settings); writeInfoLog("Got plugin settings", data?.settings);
return data?.settings; return data?.settings;
}, },
(_err: any) => undefined, (_err) => undefined,
); );
setPluginSettings(settings); setPluginSettings(settings);
return settings; return settings;
@@ -367,11 +368,11 @@ export const useSettings = (api: Api | null) => {
}; };
}, [_settings, pluginSettings]); }, [_settings, pluginSettings]);
return [ return {
settings, settings,
updateSettings, updateSettings,
pluginSettings, pluginSettings,
setPluginSettings, setPluginSettings,
refreshStreamyfinPluginSettings, refreshStreamyfinPluginSettings,
] as const; };
}; };