Compare commits

...

29 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
Gauvain
5676359138 Merge branch 'develop' into view-password 2025-09-02 15:34:47 +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
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
79 changed files with 853 additions and 544 deletions

View File

@@ -5,8 +5,6 @@
and to ensure all necessary checks are completed before merging.
-->
# 📦 Pull Request
## 🔖 Summary
<!--
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)
-->
## 📋 Details
<!--
Provide more context or background. Explain any non-obvious decisions.
Include screenshots or GIFs for UI changes if applicable.
-->
### ⚠️ Breaking Changes
<!-- 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. -->
### 🖼️ 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
<!--

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.35.0",
"version": "0.35.1",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -37,7 +37,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 66,
"versionCode": 67,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.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";
export default function page() {
const [settings, updateSettings, pluginSettings] = useSettings(null);
const { settings, updateSettings, pluginSettings } = useSettings();
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);

View File

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

View File

@@ -21,7 +21,7 @@ export default function page() {
const { t } = useTranslation();
const [settings, updateSettings, pluginSettings] = useSettings(null);
const { settings, updateSettings, pluginSettings } = useSettings();
const queryClient = useQueryClient();
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() {
const local = useLocalSearchParams();
const { jellyseerrApi } = useJellyseerr();
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
const { companyId, image, type } = local as unknown as {
companyId: string;
@@ -53,7 +53,10 @@ export default function page() {
uniqBy(
data?.pages
?.filter((p) => p?.results.length)
.flatMap((p) => p?.results ?? []),
.flatMap(
(p) =>
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [],
),
"id",
) ?? [],
[data],
@@ -98,9 +101,7 @@ export default function page() {
}}
/>
}
renderItem={(item, _index) => (
<JellyseerrPoster item={item as MovieResult | TvResult} />
)}
renderItem={(item, _index) => <JellyseerrPoster item={item} />}
/>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -230,7 +230,7 @@ const queryClient = new QueryClient({
});
function Layout() {
const [settings] = useSettings(null);
const { settings } = useSettings();
const [user] = useAtom(userAtom);
const [api] = useAtom(apiAtom);
const appState = useRef(AppState.currentState);
@@ -269,6 +269,15 @@ function Layout() {
await Notifications?.setNotificationChannelAsync("default", {
name: "default",
});
// Create dedicated channel for download notifications
console.log("Setting android notification channel 'downloads'");
await Notifications?.setNotificationChannelAsync("downloads", {
name: "Downloads",
importance: Notifications.AndroidImportance.DEFAULT,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF231F7C",
});
}
const granted = await checkAndRequestPermissions();

View File

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

182
bun.lock
View File

@@ -7,7 +7,7 @@
"@bottom-tabs/react-navigation": "^0.9.2",
"@expo/metro-runtime": "~5.0.4",
"@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",
"@jellyfin/sdk": "^0.11.0",
"@kesha-antonov/react-native-background-downloader": "^3.2.6",
@@ -76,33 +76,33 @@
"react-native-video": "6.14.1",
"react-native-volume-manager": "^2.0.8",
"react-native-web": "^0.20.0",
"sonner-native": "^0.21.0",
"sonner-native": "^0.21.1",
"tailwindcss": "3.3.2",
"use-debounce": "^10.0.4",
"zeego": "^3.0.6",
"zod": "^4.1.3",
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@biomejs/biome": "^2.2.2",
"@react-native-community/cli": "^20.0.0",
"@react-native-tvos/config-tv": "^0.1.1",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.15",
"@babel/core": "7.28.3",
"@biomejs/biome": "2.2.2",
"@react-native-community/cli": "20.0.1",
"@react-native-tvos/config-tv": "0.1.3",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.20",
"@types/react": "~19.0.10",
"@types/react-test-renderer": "^19.0.0",
"cross-env": "^10.0.0",
"expo-doctor": "^1.17.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.5",
"postinstall-postinstall": "^2.1.0",
"@types/react-test-renderer": "19.1.0",
"cross-env": "10.0.0",
"expo-doctor": "1.17.2",
"husky": "9.1.7",
"lint-staged": "16.1.6",
"postinstall": "0.11.2",
"react-test-renderer": "19.1.1",
"typescript": "~5.8.3",
"typescript": "5.8.3",
},
},
},
"trustedDependencies": [
"postinstall-postinstall",
"postinstall",
],
"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=="],
@@ -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=="],
"@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=="],
"@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/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=="],
@@ -391,6 +393,10 @@
"@ide/backoff": ["@ide/backoff@1.0.0", "", {}, "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g=="],
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
@@ -511,29 +517,29 @@
"@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=="],
@@ -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-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=="],
@@ -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=="],
"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=="],
@@ -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=="],
"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=="],
@@ -1331,9 +1337,9 @@
"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=="],
@@ -1409,7 +1415,7 @@
"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=="],
@@ -1511,7 +1517,7 @@
"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=="],
@@ -1553,7 +1559,7 @@
"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=="],
@@ -1781,7 +1787,7 @@
"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=="],
@@ -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/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/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/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/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/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/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/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/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=="],
"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-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=="],
"glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -2177,7 +2191,7 @@
"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=="],
@@ -2207,7 +2221,7 @@
"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=="],
@@ -2225,6 +2239,8 @@
"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/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=="],
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
@@ -2271,6 +2289,8 @@
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -2289,6 +2309,10 @@
"@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/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/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/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=="],
"@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/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=="],
"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=="],
@@ -2377,8 +2435,12 @@
"node-vibrant/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -2387,6 +2449,12 @@
"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=="],
"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=="],
"@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/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/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/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=="],
"@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=="],
"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=="],
"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/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=="],
"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=="],
"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=="],
"@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=="],
"@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=="],
"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 [user] = useAtom(userAtom);
const [queue, _setQueue] = useAtom(queueAtom);
const [settings] = useSettings(null);
const { settings } = useSettings();
const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
const { processes, startBackgroundDownload, getDownloadedItems } =

View File

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

View File

@@ -5,7 +5,7 @@ import { useAtom } from "jotai";
import type React from "react";
import { useTranslation } from "react-i18next";
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 { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
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 widthProgress = useSharedValue(0);
const colorChangeProgress = useSharedValue(0);
const [settings, updateSettings] = useSettings(null);
const { settings, updateSettings } = useSettings();
const lightHapticFeedback = useHaptic("light");
const goToPlayer = useCallback(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
import { useActionSheet } from "@expo/react-native-action-sheet";
import type {
BaseItemDto,
BaseItemPerson,
} from "@jellyfin/sdk/lib/generated-client/models";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useRouter, useSegments } from "expo-router";
import { type PropsWithChildren, useCallback } from "react";
import { TouchableOpacity, type TouchableOpacityProps } from "react-native";
@@ -14,10 +11,7 @@ interface Props extends TouchableOpacityProps {
isOffline?: boolean;
}
export const itemRouter = (
item: BaseItemDto | BaseItemPerson,
from: string,
) => {
export const itemRouter = (item: BaseItemDto, from: string) => {
if ("CollectionType" in item && item.CollectionType === "livetv") {
return `/(auth)/(tabs)/${from}/livetv`;
}
@@ -26,8 +20,8 @@ export const itemRouter = (
return `/(auth)/(tabs)/${from}/series/${item.Id}`;
}
if (item.Type === "Person" || item.Type === "Actor") {
return `/(auth)/(tabs)/${from}/actors/${item.Id}`;
if (item.Type === "Person") {
return `/(auth)/(tabs)/${from}/persons/${item.Id}`;
}
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 { useMemo } from "react";
import {
ActivityIndicator,
TouchableOpacity,
type TouchableOpacityProps,
View,
type ViewProps,
} from "react-native";
import { toast } from "sonner-native";
import { View, type ViewProps } from "react-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;
};
import { DownloadCard } from "./DownloadCard";
interface ActiveDownloadsProps extends ViewProps {}
@@ -52,163 +34,3 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) {
</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 && (
<Input
placeholder={t("search.search")}
className='my-2 border-neutral-800 border'
extraClassName='my-2 border-neutral-800 border'
value={search}
onChangeText={(text) => {
setSearch(text);

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
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";
interface Props extends ViewProps {
@@ -34,7 +34,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity
disabled={disabled}
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" : ""
}`}
{...(viewProps as any)}
@@ -54,7 +54,7 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
);
return (
<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" : ""
}`}
{...viewProps}
@@ -106,7 +106,10 @@ const ListItemContent = ({
{title}
</Text>
{subtitle && (
<Text className='text-[#9899A1] text-sm mt-0.5' numberOfLines={2}>
<Text
className='text-[#9899A1] text-[12px] mt-0.5'
numberOfLines={2}
>
{subtitle}
</Text>
)}

View File

@@ -12,7 +12,7 @@ import { useAtom } from "jotai";
import { useCallback } from "react";
import { View, type ViewProps } from "react-native";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { InfiniteHorizontalScroll } from "../common/InfiniteHorrizontalScroll";
import { InfiniteHorizontalScroll } from "../common/InfiniteHorizontalScroll";
import { Text } from "../common/Text";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
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 { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type {
MovieResult,
TvResult,
@@ -27,7 +28,7 @@ import type {
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
interface Props extends ViewProps {
item?: MovieResult | TvResult | MovieDetails | TvDetails;
item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast;
horizontal?: boolean;
showDownloadInfo?: boolean;
mediaRequest?: MediaRequest;

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import { orderBy } from "lodash";
import type React from "react";
import { useCallback, useMemo, useState } from "react";
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 { Tags } from "@/components/GenreTags";
import { dateOpts } from "@/components/jellyseerr/DetailFacts";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { t } from "i18next";
import { useCallback, useMemo } from "react";
import { toast } from "sonner-native";
import { defaultValues, Settings } from "@/utils/atoms/settings";
import { useSettings } from "@/utils/atoms/settings";
import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
import {
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 {
CombinedCredit,
PersonCreditCast,
PersonDetails,
} from "@/utils/jellyseerr/server/models/Person";
import type {
@@ -416,10 +417,8 @@ export class JellyseerrApi {
const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER));
export const useJellyseerr = (
settings: Settings = defaultValues,
updateSettings: (update: Partial<Settings>) => void = () => {},
) => {
export const useJellyseerr = () => {
const { settings, updateSettings } = useSettings();
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom);
const queryClient = useQueryClient();
@@ -468,49 +467,47 @@ export const useJellyseerr = (
[jellyseerrApi],
);
const isJellyseerrResult = (
const isJellyseerrMovieOrTvResult = (
items: any | null | undefined,
): items is Results => {
): items is MovieResult | TvResult => {
return (
items &&
Object.hasOwn(items, "mediaType") &&
Object.values(MediaType).includes(items.mediaType as MediaType)
(items.mediaType === MediaType.MOVIE || items.mediaType === MediaType.TV)
);
};
const getTitle = (
item?: TvResult | TvDetails | MovieResult | MovieDetails,
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
) => {
return isJellyseerrResult(item)
return isJellyseerrMovieOrTvResult(item)
? item.mediaType === MediaType.MOVIE
? item?.title
: item?.name
: item?.mediaInfo.mediaType === MediaType.MOVIE
: item?.mediaInfo?.mediaType === MediaType.MOVIE
? (item as MovieDetails)?.title
: (item as TvDetails)?.name;
};
const getYear = (
item?: TvResult | TvDetails | MovieResult | MovieDetails,
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
) => {
return new Date(
(isJellyseerrResult(item)
(isJellyseerrMovieOrTvResult(item)
? item.mediaType === MediaType.MOVIE
? item?.releaseDate
: item?.firstAirDate
: item?.mediaInfo.mediaType === MediaType.MOVIE
: item?.mediaInfo?.mediaType === MediaType.MOVIE
? (item as MovieDetails)?.releaseDate
: (item as TvDetails)?.firstAirDate) || "",
)?.getFullYear?.();
};
const getMediaType = (
item?: TvResult | TvDetails | MovieResult | MovieDetails,
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
): MediaType => {
return isJellyseerrResult(item)
? item.mediaType === "movie"
? MediaType.MOVIE
: MediaType.TV
return isJellyseerrMovieOrTvResult(item)
? (item.mediaType as MediaType)
: item?.mediaInfo?.mediaType;
};
@@ -528,7 +525,7 @@ export const useJellyseerr = (
jellyseerrUser,
setJellyseerrUser,
clearAllJellyseerData,
isJellyseerrResult,
isJellyseerrMovieOrTvResult,
getTitle,
getYear,
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 { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useNetInfo } from "@react-native-community/netinfo";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { useMemo } from "react";
import { useDownload } from "@/providers/DownloadProvider";
import { DownloadedItem } from "@/providers/Downloads/types";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useNetworkStatus } from "./useNetworkStatus";
interface PlaybackManagerProps {
item?: BaseItemDto | null;
@@ -65,12 +65,12 @@ export const usePlaybackManager = ({
}: PlaybackManagerProps = {}) => {
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const netInfo = useNetInfo();
const { isConnected } = useNetworkStatus();
const { getDownloadedItemById, updateDownloadedItem, getDownloadedItems } =
useDownload();
/** 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
const { data: adjacentItems } = useQuery({

View File

@@ -1,8 +1,8 @@
import { getItemsApi, getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useNetInfo } from "@react-native-community/netinfo";
import { useAtomValue } from "jotai";
import { useDownload } from "@/providers/DownloadProvider";
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
@@ -11,8 +11,8 @@ import { apiAtom, userAtom } from "../providers/JellyfinProvider";
export const useTwoWaySync = () => {
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const netInfo = useNetInfo();
const { getDownloadedItemById, updateDownloadedItem } = useDownload();
const { isConnected } = useNetworkStatus();
/**
* 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).
*/
const syncPlaybackState = async (itemId: string): Promise<boolean> => {
if (!api || !user || !netInfo.isConnected) {
if (!api || !user || !isConnected) {
// Cannot sync if offline or not logged in
return false;
}

View File

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

View File

@@ -4,6 +4,7 @@ import type {
} from "@jellyfin/sdk/lib/generated-client/models";
import * as Application from "expo-application";
import * as FileSystem from "expo-file-system";
import * as Notifications from "expo-notifications";
import { router } from "expo-router";
import { atom, useAtom } from "jotai";
import { throttle } from "lodash";
@@ -87,9 +88,72 @@ function useDownloadProvider() {
const { saveSeriesPrimaryImage } = useDownloadHelper();
const { saveImage } = useImageStorage();
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
const [settings] = useSettings(null);
const { settings } = useSettings();
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.
const updateProgress = async () => {
const tasks = await BackGroundDownloader.checkForExistingDownloads();
@@ -418,6 +482,21 @@ function useDownloadProvider() {
}
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(
t("home.downloads.toasts.download_completed_for_item", {
item: process.item.Name,
@@ -425,8 +504,25 @@ function useDownloadProvider() {
);
removeProcess(process.id);
})
.error((error: any) => {
.error(async (error: any) => {
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(
t("home.downloads.toasts.download_failed_for_item", {
item: process.item.Name,
@@ -435,7 +531,7 @@ function useDownloadProvider() {
removeProcess(process.id);
});
},
[authHeader],
[authHeader, sendDownloadNotification, getNotificationContent],
);
const manageDownloadQueue = useCallback(() => {

View File

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

View File

@@ -22,7 +22,10 @@
"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?",
"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": {
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",

View File

@@ -9,6 +9,7 @@ import {
hasPermission,
Permission,
} from "@/utils/jellyseerr/server/lib/permissions";
import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import type {
MovieResult,
TvResult,
@@ -18,7 +19,7 @@ import type { MovieDetails } from "../jellyseerr/server/models/Movie";
import type { TvDetails } from "../jellyseerr/server/models/Tv";
export const useJellyseerrCanRequest = (
item?: MovieResult | TvResult | MovieDetails | TvDetails,
item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast,
) => {
const { jellyseerrUser } = useJellyseerr();

View File

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

View File

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