diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx
index 1165716d..5ad41f97 100644
--- a/app/(auth)/(tabs)/(home)/_layout.tsx
+++ b/app/(auth)/(tabs)/(home)/_layout.tsx
@@ -66,12 +66,6 @@ export default function IndexLayout() {
title: t("home.settings.settings_title"),
}}
/>
-
(
{},
);
- const { downloadedFiles, deleteItems } = useDownload();
+ const { getDownloadedItems, deleteItems } = useDownload();
const series = useMemo(() => {
try {
return (
- downloadedFiles
+ getDownloadedItems()
?.filter((f) => f.item.SeriesId === seriesId)
?.sort(
(a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber!,
@@ -37,7 +37,7 @@ export default function page() {
} catch {
return [];
}
- }, [downloadedFiles]);
+ }, [getDownloadedItems]);
const seasonIndex =
seasonIndexState[series?.[0]?.item?.ParentId ?? ""] ||
diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx
index 1f2cedb9..ae5c32f5 100644
--- a/app/(auth)/(tabs)/(home)/downloads/index.tsx
+++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx
@@ -10,33 +10,34 @@ import { useAtom } from "jotai";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Alert, ScrollView, TouchableOpacity, View } from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
import { toast } from "sonner-native";
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
+import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { ActiveDownloads } from "@/components/downloads/ActiveDownloads";
import { DownloadSize } from "@/components/downloads/DownloadSize";
import { MovieCard } from "@/components/downloads/MovieCard";
import { SeriesCard } from "@/components/downloads/SeriesCard";
-import { type DownloadedItem, useDownload } from "@/providers/DownloadProvider";
+import { useDownload } from "@/providers/DownloadProvider";
+import { type DownloadedItem } from "@/providers/Downloads/types";
import { queueAtom } from "@/utils/atoms/queue";
-import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { writeToLog } from "@/utils/log";
export default function page() {
const navigation = useNavigation();
const { t } = useTranslation();
const [queue, setQueue] = useAtom(queueAtom);
- const { removeProcess, downloadedFiles, deleteFileByType, deleteAllFiles } =
- useDownload();
+ const {
+ removeProcess,
+ getDownloadedItems,
+ deleteFileByType,
+ deleteAllFiles,
+ } = useDownload();
const router = useRouter();
- const [settings] = useSettings();
const bottomSheetModalRef = useRef(null);
const [showMigration, setShowMigration] = useState(false);
- const insets = useSafeAreaInsets();
-
const migration_20241124 = () => {
Alert.alert(
t("home.downloads.new_app_version_requires_re_download"),
@@ -44,7 +45,10 @@ export default function page() {
[
{
text: t("home.downloads.back"),
- onPress: () => setShowMigration(false) || router.back(),
+ onPress: () => {
+ setShowMigration(false);
+ router.back();
+ },
},
{
text: t("home.downloads.delete"),
@@ -58,6 +62,8 @@ export default function page() {
);
};
+ const downloadedFiles = getDownloadedItems();
+
const movies = useMemo(() => {
try {
return downloadedFiles?.filter((f) => f.item.Type === "Movie") || [];
@@ -127,16 +133,10 @@ export default function page() {
return (
<>
-
-
-
- {settings?.downloadMethod === DownloadMethod.Remux && (
+
+
+
+
{t("home.downloads.queue")}
@@ -180,70 +180,74 @@ export default function page() {
)}
- )}
-
-
-
- {movies.length > 0 && (
-
-
-
- {t("home.downloads.movies")}
-
-
- {movies?.length}
-
-
-
-
- {movies?.map((item) => (
-
-
-
- ))}
-
-
+
- )}
- {groupedBySeries.length > 0 && (
-
-
-
- {t("home.downloads.tvseries")}
-
-
-
- {groupedBySeries?.length}
+
+ {movies.length > 0 && (
+
+
+
+ {t("home.downloads.movies")}
+
+ {movies?.length}
+
+
+
+ {movies?.map((item) => (
+
+
+
+ ))}
+
+
-
-
- {groupedBySeries?.map((items) => (
-
- i.item)}
- key={items[0].item.SeriesId}
- />
-
- ))}
+ )}
+ {groupedBySeries.length > 0 && (
+
+
+
+ {t("home.downloads.tvseries")}
+
+
+
+ {groupedBySeries?.length}
+
+
-
-
- )}
- {downloadedFiles?.length === 0 && (
-
-
- {t("home.downloads.no_downloaded_items")}
-
-
- )}
-
-
+
+
+ {groupedBySeries?.map((items) => (
+
+ i.item)}
+ key={items[0].item.SeriesId}
+ />
+
+ ))}
+
+
+
+ )}
+ {downloadedFiles?.length === 0 && (
+
+
+ {t("home.downloads.no_downloaded_items")}
+
+
+ )}
+
+
+
["asc", "desc"]}
set={(values) => setOrder(values[0])}
@@ -83,7 +86,7 @@ export default function page() {
showSearch={false}
/>
defaultLevels}
set={setLevels}
diff --git a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx b/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx
deleted file mode 100644
index aff5f3ca..00000000
--- a/app/(auth)/(tabs)/(home)/settings/optimized-server/page.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { useMutation } from "@tanstack/react-query";
-import { useNavigation } from "expo-router";
-import { useAtom } from "jotai";
-import { useEffect, useState } from "react";
-import { useTranslation } from "react-i18next";
-import { ActivityIndicator, TouchableOpacity } from "react-native";
-import { toast } from "sonner-native";
-import { Text } from "@/components/common/Text";
-import DisabledSetting from "@/components/settings/DisabledSetting";
-import { OptimizedServerForm } from "@/components/settings/OptimizedServerForm";
-import { apiAtom } from "@/providers/JellyfinProvider";
-import { useSettings } from "@/utils/atoms/settings";
-import { getOrSetDeviceId } from "@/utils/device";
-import { getStatistics } from "@/utils/optimize-server";
-
-export default function page() {
- const navigation = useNavigation();
-
- const { t } = useTranslation();
-
- const [api] = useAtom(apiAtom);
- const [settings, updateSettings, pluginSettings] = useSettings();
-
- const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
- useState(settings?.optimizedVersionsServerUrl || "");
-
- const saveMutation = useMutation({
- mutationFn: async (newVal: string) => {
- if (newVal.length === 0 || !newVal.startsWith("http")) {
- toast.error(t("home.settings.toasts.invalid_url"));
- return;
- }
-
- const updatedUrl = newVal.endsWith("/") ? newVal : `${newVal}/`;
-
- updateSettings({
- optimizedVersionsServerUrl: updatedUrl,
- });
-
- return await getStatistics({
- url: updatedUrl,
- authHeader: api?.accessToken,
- deviceId: getOrSetDeviceId(),
- });
- },
- onSuccess: (data) => {
- if (data) {
- toast.success(t("home.settings.toasts.connected"));
- } else {
- toast.error(t("home.settings.toasts.could_not_connect"));
- }
- },
- onError: () => {
- toast.error(t("home.settings.toasts.could_not_connect"));
- },
- });
-
- const onSave = (newVal: string) => {
- saveMutation.mutate(newVal);
- };
-
- useEffect(() => {
- if (!pluginSettings?.optimizedVersionsServerUrl?.locked) {
- navigation.setOptions({
- title: t("home.settings.downloads.optimized_server"),
- headerRight: () =>
- saveMutation.isPending ? (
-
- ) : (
- onSave(optimizedVersionsServerUrl)}
- >
-
- {t("home.settings.downloads.save_button")}
-
-
- ),
- });
- }
- }, [navigation, optimizedVersionsServerUrl, saveMutation.isPending]);
-
- return (
-
-
-
- );
-}
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx
index 9260ccc8..798fa2cc 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/collections/[collectionId].tsx
@@ -112,7 +112,7 @@ const page: React.FC = () => {
recursive: true,
genres: selectedGenres,
tags: selectedTags,
- years: selectedYears.map((year) => Number.parseInt(year)),
+ years: selectedYears.map((year) => Number.parseInt(year, 10)),
includeItemTypes: ["Movie", "Series"],
});
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx
index d55c05d4..81f123bf 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/items/page.tsx
@@ -1,7 +1,4 @@
-import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
-import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams } from "expo-router";
-import { useAtom } from "jotai";
import type React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
@@ -14,30 +11,16 @@ import Animated, {
} from "react-native-reanimated";
import { Text } from "@/components/common/Text";
import { ItemContent } from "@/components/ItemContent";
-import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
+import { useItemQuery } from "@/hooks/useItemQuery";
const Page: React.FC = () => {
- const [api] = useAtom(apiAtom);
- const [user] = useAtom(userAtom);
const { id } = useLocalSearchParams() as { id: string };
const { t } = useTranslation();
- const { data: item, isError } = useQuery({
- queryKey: ["item", id],
- queryFn: async () => {
- if (!api || !user || !id) return;
- const res = await getUserLibraryApi(api).getItem({
- itemId: id,
- userId: user?.Id,
- });
+ const { offline } = useLocalSearchParams() as { offline?: string };
+ const isOffline = offline === "true";
- return res.data;
- },
- staleTime: 0,
- refetchOnMount: true,
- refetchOnWindowFocus: true,
- refetchOnReconnect: true,
- });
+ const { data: item, isError } = useItemQuery(id, isOffline);
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => {
@@ -107,7 +90,7 @@ const Page: React.FC = () => {
- {item && }
+ {item && }
);
};
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx
index 17c8cf69..bf22d464 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx
@@ -69,10 +69,18 @@ const page: React.FC = () => {
seriesId: item?.Id!,
userId: user?.Id!,
enableUserData: true,
- fields: ["MediaSources", "MediaStreams", "Overview"],
+ // Note: Including trick play is necessary to enable trick play downloads
+ fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
});
return res?.data.Items || [];
},
+ select: (data) =>
+ // This needs to be sorted by parent index number and then index number, that way we can download the episodes in the correct order.
+ [...(data || [])].sort(
+ (a, b) =>
+ (a.ParentIndexNumber ?? 0) - (b.ParentIndexNumber ?? 0) ||
+ (a.IndexNumber ?? 0) - (b.IndexNumber ?? 0),
+ ),
staleTime: 60,
enabled: !!api && !!user?.Id && !!item?.Id,
});
@@ -136,7 +144,7 @@ const page: React.FC = () => {
resizeMode: "contain",
}}
/>
- ) : null
+ ) : undefined
}
>
diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
index 6800eb31..2f58d74c 100644
--- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
+++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
@@ -168,7 +168,7 @@ const Page = () => {
fields: ["PrimaryImageAspectRatio", "SortName"],
genres: selectedGenres,
tags: selectedTags,
- years: selectedYears.map((year) => Number.parseInt(year)),
+ years: selectedYears.map((year) => Number.parseInt(year, 10)),
includeItemTypes: itemType ? [itemType] : undefined,
});
diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx
index 765aa2e0..7227b5b4 100644
--- a/app/(auth)/(tabs)/(search)/index.tsx
+++ b/app/(auth)/(tabs)/(search)/index.tsx
@@ -10,6 +10,7 @@ import { useAtom } from "jotai";
import {
useCallback,
useEffect,
+ useId,
useLayoutEffect,
useMemo,
useRef,
@@ -58,6 +59,9 @@ export default function search() {
const { t } = useTranslation();
+ const searchFilterId = useId();
+ const orderFilterId = useId();
+
const { q } = params as { q: string };
const [searchType, setSearchType] = useState("Library");
@@ -313,7 +317,7 @@ export default function search() {
debouncedSearch.length > 0 && (
Object.keys(JellyseerrSearchSort).filter((v) =>
@@ -329,7 +333,7 @@ export default function search() {
showSearch={false}
/>
["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])}
diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx
index 3f441b75..c7cff282 100644
--- a/app/(auth)/player/direct-player.tsx
+++ b/app/(auth)/player/direct-player.tsx
@@ -2,7 +2,6 @@ import {
type BaseItemDto,
type MediaSourceInfo,
PlaybackOrder,
- type PlaybackProgressInfo,
PlaybackStartInfo,
RepeatMode,
} from "@jellyfin/sdk/lib/generated-client";
@@ -22,8 +21,8 @@ import { BITRATES } from "@/components/BitrateSelector";
import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
import { Controls } from "@/components/video-player/controls/Controls";
-import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
import { useHaptic } from "@/hooks/useHaptic";
+import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
import { useWebSocket } from "@/hooks/useWebsockets";
import { VlcPlayerView } from "@/modules";
@@ -33,18 +32,16 @@ import type {
ProgressUpdatePayload,
VlcPlayerViewRef,
} from "@/modules/VlcPlayer.types";
+import { useDownload } from "@/providers/DownloadProvider";
+import { DownloadedItem } from "@/providers/Downloads/types";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { writeToLog } from "@/utils/log";
import { storage } from "@/utils/mmkv";
-import generateDeviceProfile from "@/utils/profiles/native";
+import { generateDeviceProfile } from "@/utils/profiles/native";
import { msToTicks, ticksToSeconds } from "@/utils/time";
-const downloadProvider = !Platform.isTV
- ? require("@/providers/DownloadProvider")
- : { useDownload: () => null };
-
const IGNORE_SAFE_AREAS_KEY = "video_player_ignore_safe_areas";
export default function page() {
@@ -74,7 +71,7 @@ export default function page() {
? null
: require("react-native-volume-manager");
- const getDownloadedItem = downloadProvider.useDownload();
+ const downloadUtils = useDownload();
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
@@ -111,6 +108,7 @@ export default function page() {
const [settings] = useSettings();
const insets = useSafeAreaInsets();
const offline = offlineStr === "true";
+ const playbackManager = usePlaybackManager();
const audioIndex = audioIndexStr
? Number.parseInt(audioIndexStr, 10)
@@ -123,18 +121,21 @@ export default function page() {
: BITRATES[0].value;
const [item, setItem] = useState(null);
+ const [downloadedItem, setDownloadedItem] = useState(
+ null,
+ );
const [itemStatus, setItemStatus] = useState({
isLoading: true,
isError: false,
});
- /** Gets the initial playback position from the URL or the item's user data. */
+ /** Gets the initial playback position from the URL. */
const getInitialPlaybackTicks = useCallback((): number => {
if (playbackPositionFromUrl) {
return Number.parseInt(playbackPositionFromUrl, 10);
}
return item?.UserData?.PlaybackPositionTicks ?? 0;
- }, [playbackPositionFromUrl, item]);
+ }, [playbackPositionFromUrl]);
useEffect(() => {
const fetchItemData = async () => {
@@ -142,8 +143,11 @@ export default function page() {
try {
let fetchedItem: BaseItemDto | null = null;
if (offline && !Platform.isTV) {
- const data = await getDownloadedItem.getDownloadedItem(itemId);
- if (data) fetchedItem = data.item as BaseItemDto;
+ const data = downloadUtils.getDownloadedItemById(itemId);
+ if (data) {
+ fetchedItem = data.item as BaseItemDto;
+ setDownloadedItem(data);
+ }
} else {
const res = await getUserLibraryApi(api!).getItem({
itemId,
@@ -179,18 +183,20 @@ export default function page() {
useEffect(() => {
const fetchStreamData = async () => {
setStreamStatus({ isLoading: true, isError: false });
- const native = await generateDeviceProfile();
try {
let result: Stream | null = null;
- if (offline && !Platform.isTV) {
- const data = await getDownloadedItem.getDownloadedItem(itemId);
- if (!data?.mediaSource) return;
- const url = await getDownloadedFileUrl(data.item.Id!);
+ if (offline && downloadedItem && downloadedItem.mediaSource) {
+ const url = downloadedItem.videoFilePath;
if (item) {
- result = { mediaSource: data.mediaSource, sessionId: "", url };
+ result = {
+ mediaSource: downloadedItem.mediaSource,
+ sessionId: "",
+ url: url,
+ };
}
} else {
- if (!item) return;
+ const native = generateDeviceProfile();
+ const transcoding = generateDeviceProfile({ transcode: true });
const res = await getStreamUrl({
api,
item,
@@ -200,7 +206,7 @@ export default function page() {
maxStreamingBitrate: bitrateValue,
mediaSourceId: mediaSourceId,
subtitleStreamIndex: subtitleIndex,
- deviceProfile: native,
+ deviceProfile: bitrateValue ? transcoding : native,
});
if (!res) return;
const { mediaSource, sessionId, url } = res;
@@ -221,26 +227,39 @@ export default function page() {
}
};
fetchStreamData();
- }, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]);
+ }, [
+ itemId,
+ mediaSourceId,
+ bitrateValue,
+ api,
+ item,
+ user?.Id,
+ downloadedItem,
+ ]);
useEffect(() => {
- if (!stream) return;
-
+ if (!stream || !api) return;
const reportPlaybackStart = async () => {
- await getPlaystateApi(api!).reportPlaybackStart({
+ await getPlaystateApi(api).reportPlaybackStart({
playbackStartInfo: currentPlayStateInfo() as PlaybackStartInfo,
});
};
-
reportPlaybackStart();
- }, [stream]);
+ }, [stream, api]);
const togglePlay = async () => {
lightHapticFeedback();
setIsPlaying(!isPlaying);
if (isPlaying) {
await videoRef.current?.pause();
- reportPlaybackProgress();
+ playbackManager.reportPlaybackProgress(
+ item?.Id!,
+ msToTicks(progress.get()),
+ {
+ AudioStreamIndex: audioIndex ?? -1,
+ SubtitleStreamIndex: subtitleIndex ?? -1,
+ },
+ );
} else {
videoRef.current?.play();
await getPlaystateApi(api!).reportPlaybackStart({
@@ -250,7 +269,6 @@ export default function page() {
};
const reportPlaybackStopped = useCallback(async () => {
- if (offline) return;
const currentTimeInTicks = msToTicks(progress.get());
await getPlaystateApi(api!).onPlaybackStopped({
itemId: item?.Id!,
@@ -258,8 +276,6 @@ export default function page() {
positionTicks: currentTimeInTicks,
playSessionId: stream?.sessionId!,
});
-
- revalidateProgressCache();
}, [
api,
item,
@@ -274,6 +290,7 @@ export default function page() {
reportPlaybackStopped();
setIsPlaybackStopped(true);
videoRef.current?.stop();
+ revalidateProgressCache();
}, [videoRef, reportPlaybackStopped]);
useEffect(() => {
@@ -317,10 +334,16 @@ export default function page() {
playbackPosition: msToTicks(currentTime).toString(),
});
- if (offline) return;
- if (!item?.Id || !stream) return;
+ if (!item?.Id) return;
- reportPlaybackProgress();
+ playbackManager.reportPlaybackProgress(
+ item.Id,
+ msToTicks(progress.get()),
+ {
+ AudioStreamIndex: audioIndex ?? -1,
+ SubtitleStreamIndex: subtitleIndex ?? -1,
+ },
+ );
},
[
item?.Id,
@@ -340,28 +363,10 @@ export default function page() {
setIsPipStarted(pipStarted);
}, []);
- const reportPlaybackProgress = useCallback(async () => {
- if (!api || offline || !stream) return;
- await getPlaystateApi(api).reportPlaybackProgress({
- playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo,
- });
- }, [
- api,
- isPlaying,
- offline,
- stream,
- item?.Id,
- audioIndex,
- subtitleIndex,
- mediaSourceId,
- progress,
- ]);
-
/** Gets the initial playback position in seconds. */
const startPosition = useMemo(() => {
- if (offline) return 0;
return ticksToSeconds(getInitialPlaybackTicks());
- }, [offline, getInitialPlaybackTicks]);
+ }, [getInitialPlaybackTicks]);
const volumeUpCb = useCallback(async () => {
if (Platform.isTV) return;
@@ -446,14 +451,28 @@ export default function page() {
const { state, isBuffering, isPlaying } = e.nativeEvent;
if (state === "Playing") {
setIsPlaying(true);
- reportPlaybackProgress();
+ if (item?.Id) {
+ playbackManager.reportPlaybackProgress(
+ item.Id,
+ msToTicks(progress.get()),
+ {
+ AudioStreamIndex: audioIndex ?? -1,
+ SubtitleStreamIndex: subtitleIndex ?? -1,
+ },
+ );
+ }
if (!Platform.isTV) await activateKeepAwakeAsync();
return;
}
if (state === "Paused") {
setIsPlaying(false);
- reportPlaybackProgress();
+ if (item?.Id) {
+ playbackManager.reportPlaybackProgress(
+ item.Id,
+ msToTicks(progress.get()),
+ );
+ }
if (!Platform.isTV) await deactivateKeepAwake();
return;
}
@@ -465,7 +484,7 @@ export default function page() {
setIsBuffering(true);
}
},
- [reportPlaybackProgress],
+ [playbackManager, item?.Id, progress],
);
const allAudio =
@@ -483,25 +502,29 @@ export default function page() {
.filter((sub: any) => sub.DeliveryMethod === "External")
.map((sub: any) => ({
name: sub.DisplayTitle,
- DeliveryUrl: api?.basePath + sub.DeliveryUrl,
+ DeliveryUrl: offline ? sub.DeliveryUrl : api?.basePath + sub.DeliveryUrl,
}));
-
+ /** The text based subtitle tracks */
const textSubs = allSubs.filter((sub) => sub.IsTextSubtitleStream);
-
+ /** The user chosen subtitle track from the server */
const chosenSubtitleTrack = allSubs.find(
(sub) => sub.Index === subtitleIndex,
);
+ /** The user chosen audio track from the server */
const chosenAudioTrack = allAudio.find((audio) => audio.Index === audioIndex);
-
+ /** Whether the stream we're playing is not transcoding*/
const notTranscoding = !stream?.mediaSource.TranscodingUrl;
+ /** The initial options to pass to the VLC Player */
const initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
if (
chosenSubtitleTrack &&
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
) {
+ // If not transcoding, we can the index as normal.
+ // If transcoding, we need to reverse the text based subtitles, because VLC reverses the HLS subtitles.
const finalIndex = notTranscoding
? allSubs.indexOf(chosenSubtitleTrack)
- : textSubs.indexOf(chosenSubtitleTrack);
+ : [...textSubs].reverse().indexOf(chosenSubtitleTrack);
initOptions.push(`--sub-track=${finalIndex}`);
}
@@ -562,7 +585,7 @@ export default function page() {
source={{
uri: stream?.url || "",
autoplay: true,
- isNetwork: true,
+ isNetwork: !offline,
startPosition,
externalSubtitles,
initOptions,
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 14474d63..dcc0583b 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,7 +1,6 @@
import "@/augmentations";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
-import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { Platform } from "react-native";
import i18n from "@/i18n";
import { DownloadProvider } from "@/providers/DownloadProvider";
@@ -11,7 +10,6 @@ import {
getTokenFromStorage,
JellyfinProvider,
} from "@/providers/JellyfinProvider";
-import { JobQueueProvider } from "@/providers/JobQueueProvider";
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
import { WebSocketProvider } from "@/providers/WebSocketProvider";
import { type Settings, useSettings } from "@/utils/atoms/settings";
@@ -27,7 +25,6 @@ import {
writeToLog,
} from "@/utils/log";
import { storage } from "@/utils/mmkv";
-import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
@@ -145,100 +142,24 @@ if (!Platform.isTV) {
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
console.log("TaskManager ~ trigger");
- const now = Date.now();
+ const settingsData = storage.getString("settings");
- try {
- const settingsData = storage.getString("settings");
+ if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData;
- if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData;
+ const settings: Partial = JSON.parse(settingsData);
- const settings: Partial = JSON.parse(settingsData);
- const url = settings?.optimizedVersionsServerUrl;
+ if (!settings?.autoDownload)
+ return BackgroundFetch.BackgroundFetchResult.NoData;
- if (!settings?.autoDownload || !url)
- return BackgroundFetch.BackgroundFetchResult.NoData;
+ const token = getTokenFromStorage();
+ const deviceId = getOrSetDeviceId();
+ const baseDirectory = FileSystem.documentDirectory;
- const token = getTokenFromStorage();
- const deviceId = getOrSetDeviceId();
- const baseDirectory = FileSystem.documentDirectory;
+ if (!token || !deviceId || !baseDirectory)
+ return BackgroundFetch.BackgroundFetchResult.NoData;
- if (!token || !deviceId || !baseDirectory)
- return BackgroundFetch.BackgroundFetchResult.NoData;
-
- const jobs = await getAllJobsByDeviceId({
- deviceId,
- authHeader: token,
- url,
- });
-
- console.log("TaskManager ~ Active jobs: ", jobs.length);
-
- for (const job of jobs) {
- if (job.status === "completed") {
- const downloadUrl = `${url}download/${job.id}`;
- const tasks = await BackGroundDownloader.checkForExistingDownloads();
-
- if (tasks.find((task: { id: string }) => task.id === job.id)) {
- console.log("TaskManager ~ Download already in progress: ", job.id);
- continue;
- }
-
- BackGroundDownloader.download({
- id: job.id,
- url: downloadUrl,
- destination: `${baseDirectory}${job.item.Id}.mp4`,
- headers: {
- Authorization: token,
- },
- })
- .begin(() => {
- console.log("TaskManager ~ Download started: ", job.id);
- })
- .done(() => {
- console.log("TaskManager ~ Download completed: ", job.id);
- saveDownloadedItemInfo(job.item);
- BackGroundDownloader.completeHandler(job.id);
- cancelJobById({
- authHeader: token,
- id: job.id,
- url: url,
- });
- Notifications.scheduleNotificationAsync({
- content: {
- title: job.item.Name,
- body: "Download completed",
- data: {
- url: "/downloads",
- },
- },
- trigger: null,
- });
- })
- .error((error: any) => {
- console.log("TaskManager ~ Download error: ", job.id, error);
- BackGroundDownloader.completeHandler(job.id);
- Notifications.scheduleNotificationAsync({
- content: {
- title: job.item.Name,
- body: "Download failed",
- data: {
- url: "/downloads",
- },
- },
- trigger: null,
- });
- });
- }
- }
-
- console.log(`Auto download started: ${new Date(now).toISOString()}`);
-
- // Be sure to return the successful result type!
- return BackgroundFetch.BackgroundFetchResult.NewData;
- } catch (error) {
- console.error("Background task error:", error);
- return BackgroundFetch.BackgroundFetchResult.Failed;
- }
+ // Be sure to return the successful result type!
+ return BackgroundFetch.BackgroundFetchResult.NewData;
});
}
@@ -474,85 +395,62 @@ function Layout() {
return (
-
-
-
-
-
-
-
-
-
-
- null,
- }}
- />
- null,
- }}
- />
-
-
-
-
+
+
+
+
+
+
+
+
+ null,
}}
- closeButton
/>
-
-
-
-
-
-
-
-
+ null,
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
);
}
-
-function saveDownloadedItemInfo(item: BaseItemDto) {
- try {
- const downloadedItems = storage.getString("downloadedItems");
- const items: BaseItemDto[] = downloadedItems
- ? JSON.parse(downloadedItems)
- : [];
-
- const existingItemIndex = items.findIndex((i) => i.Id === item.Id);
- if (existingItemIndex !== -1) {
- items[existingItemIndex] = item;
- } else {
- items.push(item);
- }
-
- storage.set("downloadedItems", JSON.stringify(items));
- } catch (error) {
- writeToLog("ERROR", "Failed to save downloaded item information:", error);
- console.error("Failed to save downloaded item information:", error);
- }
-}
diff --git a/bun.lock b/bun.lock
index 83323e93..627a1675 100644
--- a/bun.lock
+++ b/bun.lock
@@ -115,15 +115,15 @@
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
- "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
+ "@babel/core": ["@babel/core@7.28.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ=="],
- "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
+ "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
- "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A=="],
+ "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg=="],
"@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ=="],
@@ -135,7 +135,7 @@
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
- "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
@@ -153,13 +153,13 @@
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
- "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.27.1", "", { "dependencies": { "@babel/template": "^7.27.1", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ=="],
+ "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="],
- "@babel/helpers": ["@babel/helpers@7.28.2", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw=="],
+ "@babel/helpers": ["@babel/helpers@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw=="],
"@babel/highlight": ["@babel/highlight@7.25.9", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw=="],
- "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
+ "@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="],
@@ -217,7 +217,7 @@
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="],
- "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA=="],
+ "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg=="],
"@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="],
@@ -267,9 +267,9 @@
"@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA=="],
- "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg=="],
+ "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A=="],
- "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.28.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA=="],
+ "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg=="],
"@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="],
@@ -287,33 +287,33 @@
"@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
- "@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="],
+ "@babel/runtime": ["@babel/runtime@7.28.3", "", {}, "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA=="],
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
- "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
+ "@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
- "@babel/traverse--for-generate-function-map": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
+ "@babel/traverse--for-generate-function-map": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
- "@biomejs/biome": ["@biomejs/biome@2.1.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.4", "@biomejs/cli-darwin-x64": "2.1.4", "@biomejs/cli-linux-arm64": "2.1.4", "@biomejs/cli-linux-arm64-musl": "2.1.4", "@biomejs/cli-linux-x64": "2.1.4", "@biomejs/cli-linux-x64-musl": "2.1.4", "@biomejs/cli-win32-arm64": "2.1.4", "@biomejs/cli-win32-x64": "2.1.4" }, "bin": { "biome": "bin/biome" } }, "sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA=="],
+ "@biomejs/biome": ["@biomejs/biome@2.2.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.0", "@biomejs/cli-darwin-x64": "2.2.0", "@biomejs/cli-linux-arm64": "2.2.0", "@biomejs/cli-linux-arm64-musl": "2.2.0", "@biomejs/cli-linux-x64": "2.2.0", "@biomejs/cli-linux-x64-musl": "2.2.0", "@biomejs/cli-win32-arm64": "2.2.0", "@biomejs/cli-win32-x64": "2.2.0" }, "bin": { "biome": "bin/biome" } }, "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw=="],
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg=="],
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg=="],
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw=="],
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw=="],
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw=="],
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw=="],
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ=="],
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ=="],
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw=="],
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw=="],
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg=="],
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg=="],
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw=="],
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA=="],
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw=="],
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="],
"@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=="],
@@ -435,15 +435,15 @@
"@jimp/utils": ["@jimp/utils@0.22.12", "", { "dependencies": { "regenerator-runtime": "^0.13.3" } }, "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q=="],
- "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
- "@jridgewell/source-map": ["@jridgewell/source-map@0.3.10", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q=="],
+ "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
- "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
- "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@kesha-antonov/react-native-background-downloader": ["@kesha-antonov/react-native-background-downloader@3.2.6", "", { "peerDependencies": { "react-native": ">=0.57.0" } }, "sha512-J87PHzBh4knWTQNkCNM4LTMZ85RpMW/QSV+0LGdTxz4JmfLXoeg8R6ratbFU0DP/l8K1eL7r4S1Rc8bmqNJ3Ug=="],
@@ -455,7 +455,7 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
- "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
@@ -465,31 +465,31 @@
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
- "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw=="],
+ "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
- "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
+ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
- "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ=="],
+ "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
- "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
+ "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
- "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew=="],
+ "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
- "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="],
+ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
- "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="],
+ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
- "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
+ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
@@ -561,17 +561,17 @@
"@react-native/normalize-colors": ["@react-native/normalize-colors@0.79.5", "", {}, "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ=="],
- "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.4.4", "", { "dependencies": { "@react-navigation/elements": "^2.6.1", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.16", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-/YEBu/cZUgYAaNoSfUnqoRjpbt8NOsb5YvDiKVyTcOOAF1GTbUw6kRi+AGW1Sm16CqzabO/TF2RvN1RmPS9VHg=="],
+ "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.4.6", "", { "dependencies": { "@react-navigation/elements": "^2.6.3", "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-f4khxwcL70O5aKfZFbxyBo5RnzPFnBNSXmrrT7q9CRmvN4mHov9KFKGQ3H4xD5sLonsTBtyjvyvPfyEC4G7f+g=="],
- "@react-navigation/core": ["@react-navigation/core@7.12.3", "", { "dependencies": { "@react-navigation/routers": "^7.5.1", "escape-string-regexp": "^4.0.0", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-oEz5sL8KTYmCv8SQX1A4k75A7VzYadOCudp/ewOBqRXOmZdxDQA9JuN7baE9IVyaRW0QTVDy+N/Wnqx9F4aW6A=="],
+ "@react-navigation/core": ["@react-navigation/core@7.12.4", "", { "dependencies": { "@react-navigation/routers": "^7.5.1", "escape-string-regexp": "^4.0.0", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q=="],
- "@react-navigation/elements": ["@react-navigation/elements@2.6.1", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.16", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-kVbIo+5FaqJv6MiYUR6nQHiw+10dmmH/P10C29wrH9S6fr7k69fImHGeiOI/h7SMDJ2vjWhftyEjqYO+c2LG/w=="],
+ "@react-navigation/elements": ["@react-navigation/elements@2.6.3", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-hcPXssZg5bFD5oKX7FP0D9ZXinRgPUHkUJbTegpenSEUJcPooH1qzWJkEP22GrtO+OPDLYrCVZxEX8FcMrn4pA=="],
- "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.3.4", "", { "dependencies": { "@react-navigation/elements": "^2.6.1", "color": "^4.2.3", "react-native-tab-view": "^4.1.2" }, "peerDependencies": { "@react-navigation/native": "^7.1.16", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-sHBiIszq6FumBu8TboN+nVyWxgwbAER6UYULllbN87dDgnUtf+BucUYRAa+2pWeZBA2Q1esYl6VFj6pEFk2how=="],
+ "@react-navigation/material-top-tabs": ["@react-navigation/material-top-tabs@7.3.6", "", { "dependencies": { "@react-navigation/elements": "^2.6.3", "color": "^4.2.3", "react-native-tab-view": "^4.1.3" }, "peerDependencies": { "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", "react-native-safe-area-context": ">= 4.0.0" } }, "sha512-94r6euJ0VFnJ6Ixp4BWO9sTQjuh7dq6nEBirMRLqVZXMVZS6nsB2olw7cA8vWjQCXIM3nLNIa2t/hIzRH2yR6Q=="],
- "@react-navigation/native": ["@react-navigation/native@7.1.16", "", { "dependencies": { "@react-navigation/core": "^7.12.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-JnnK81JYJ6PiMsuBEshPGHwfagRnH8W7SYdWNrPxQdNtakkHtG4u0O9FmrOnKiPl45DaftCcH1g+OVTFFgWa0Q=="],
+ "@react-navigation/native": ["@react-navigation/native@7.1.17", "", { "dependencies": { "@react-navigation/core": "^7.12.4", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ=="],
- "@react-navigation/native-stack": ["@react-navigation/native-stack@7.3.23", "", { "dependencies": { "@react-navigation/elements": "^2.6.1", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.16", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-WQBBnPrlM0vXj5YAFnJTyrkiCyANl2KnBV8ZmUG61HkqXFwuBbnHij6eoggXH1VZkEVRxW8k0E3qqfPtEZfUjQ=="],
+ "@react-navigation/native-stack": ["@react-navigation/native-stack@7.3.25", "", { "dependencies": { "@react-navigation/elements": "^2.6.3", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.17", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-jGcgUpif0dDGwuqag6rKTdS78MiAVAy8vmQppyaAgjS05VbCfDX+xjhc8dUxSClO5CoWlDoby1c8Hw4kBfL2UA=="],
"@react-navigation/routers": ["@react-navigation/routers@7.5.1", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w=="],
@@ -589,9 +589,9 @@
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
- "@tanstack/query-core": ["@tanstack/query-core@5.83.1", "", {}, "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.85.3", "", {}, "sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ=="],
- "@tanstack/react-query": ["@tanstack/react-query@5.84.0", "", { "dependencies": { "@tanstack/query-core": "5.83.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-iPycFGLq5lltDE16Jf13Nx7SOvtfoopfOH/+Ahbdd+z4QqOfYu/SOkY86AVYVcKjneuqPxTm8e85lSGhwe0cog=="],
+ "@tanstack/react-query": ["@tanstack/react-query@5.85.3", "", { "dependencies": { "@tanstack/query-core": "5.85.3" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
@@ -621,7 +621,7 @@
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
- "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
+ "@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
"@types/react": ["@types/react@19.0.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw=="],
@@ -767,7 +767,7 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
- "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
+ "browserslist": ["browserslist@4.25.2", "", { "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA=="],
"bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="],
@@ -795,7 +795,7 @@
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001731", "", {}, "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001735", "", {}, "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -849,7 +849,7 @@
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
- "core-js-compat": ["core-js-compat@3.44.0", "", { "dependencies": { "browserslist": "^4.25.1" } }, "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA=="],
+ "core-js-compat": ["core-js-compat@3.45.0", "", { "dependencies": { "browserslist": "^4.25.1" } }, "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA=="],
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
@@ -933,7 +933,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.194", "", {}, "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.200", "", {}, "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -1175,7 +1175,7 @@
"hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
- "i18next": ["i18next@25.3.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA=="],
+ "i18next": ["i18next@25.3.6", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -1277,7 +1277,7 @@
"joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
- "jotai": ["jotai@2.12.5", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-G8m32HW3lSmcz/4mbqx0hgJIQ0ekndKWiYP7kWVKi0p6saLXdSoye+FZiOFyonnd7Q482LCzm8sMDl7Ar1NWDw=="],
+ "jotai": ["jotai@2.13.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-cRsw6kFeGC9Z/D3egVKrTXRweycZ4z/k7i2MrfCzPYsL9SIWcPXTyqv258/+Ay8VUEcihNiE/coBLE6Kic6b8A=="],
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
@@ -1303,7 +1303,7 @@
"lan-network": ["lan-network@0.1.7", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ=="],
- "launch-editor": ["launch-editor@2.11.0", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-R/PIF14L6e2eHkhvQPu7jDRCr0msfCYCxbYiLgkkAGi0dVPWuM+RrsPu0a5dpuNe0KWGL3jpAkOlv53xGfPheQ=="],
+ "launch-editor": ["launch-editor@2.11.1", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg=="],
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
@@ -1631,7 +1631,7 @@
"react-native-image-colors": ["react-native-image-colors@2.5.0", "", { "dependencies": { "node-vibrant": "^4.0.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-3zSDgNj5HaZ0PDWaXkc4BpWpZRM5N4gBsoPC7DBfM/+op69Yvwbc0S1T7CnxBWbvShtOvRE+b2BUBadVn+6z/g=="],
- "react-native-ios-context-menu": ["react-native-ios-context-menu@3.1.2", "", { "dependencies": { "@dominicstop/ts-event-emitter": "^1.1.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-ios-utilities": "*" } }, "sha512-xmwdygAlmEofBzQvIhJd5qa+2DzPznmWuwkkqkI9NJbe+cfOmIzbvLdVD5RkiayewnCX9Mp8v/muf3BRWq/T1A=="],
+ "react-native-ios-context-menu": ["react-native-ios-context-menu@3.1.3", "", { "dependencies": { "@dominicstop/ts-event-emitter": "^1.1.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-ios-utilities": "*" } }, "sha512-p65JTOxL0D8TOgTgq3A7nVhr/hQuRTtlmsH/aQ7vaOgxY4Na/QVcEF9s4wHc7y+Rcmv84bi6V6DhqxGkFFLPmA=="],
"react-native-ios-utilities": ["react-native-ios-utilities@5.1.8", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-2lWerAkd0Kn18kUAc/RaBzHnOGG1VjbKVfTR4eEXvwYFYqCS709gOg0tGUaVLsm6CAyMe7/jA+AvKMMztzHf4g=="],
@@ -1639,7 +1639,7 @@
"react-native-mmkv": ["react-native-mmkv@2.12.2", "", { "peerDependencies": { "react": "*", "react-native": ">=0.71.0" } }, "sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg=="],
- "react-native-pager-view": ["react-native-pager-view@6.8.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-XIyVEMhwq7sZqM7GobOJZXxFCfdFgVNq/CFB2rZIRNRSVPJqE1k1fsc8xfQKfdzsp6Rpt6I7VOIvhmP7/YHdVg=="],
+ "react-native-pager-view": ["react-native-pager-view@6.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw=="],
"react-native-reanimated": ["react-native-reanimated@3.16.7", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-qoUUQOwE1pHlmQ9cXTJ2MX9FQ9eHllopCLiWOkDkp6CER95ZWeXhJCP4cSm6AD4jigL5jHcZf/SkWrg8ttZUsw=="],
@@ -1651,7 +1651,7 @@
"react-native-svg": ["react-native-svg@15.11.2", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw=="],
- "react-native-tab-view": ["react-native-tab-view@4.1.2", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-uzC1hxZGNXeQay8rSjCc7egnoYGHRpB/Y1tAwK5/nnZwrziKry7T6+gNscZgoq88+7Aag/JeNOifdWMZyRclOA=="],
+ "react-native-tab-view": ["react-native-tab-view@4.1.3", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-COj2HBeM4IqKCAadUdZAUWrFyO8++wlgObsgOt6xrwqdEnu9HX/74uesC0MGlgwIalFffXqTh5F3CC3pUjFPug=="],
"react-native-udp": ["react-native-udp@4.1.7", "", { "dependencies": { "buffer": "^5.6.0", "events": "^3.1.0" } }, "sha512-NUE3zewu61NCdSsLlj+l0ad6qojcVEZPT4hVG/x6DU9U4iCzwtfZSASh9vm7teAcVzLkdD+cO3411LHshAi/wA=="],
@@ -1895,7 +1895,7 @@
"undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
- "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
+ "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
@@ -1999,8 +1999,6 @@
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
- "@babel/helper-module-imports/@babel/types": ["@babel/types@7.19.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } }, "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA=="],
-
"@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
@@ -2081,8 +2079,6 @@
"@react-native-community/cli-doctor/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
- "@react-native-community/cli-doctor/yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
-
"@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -2193,7 +2189,7 @@
"nativewind/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
- "node-vibrant/@types/node": ["@types/node@18.19.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="],
+ "node-vibrant/@types/node": ["@types/node@18.19.122", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA=="],
"npm-package-arg/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -2201,8 +2197,6 @@
"postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
- "postcss-load-config/yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
-
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx
index e840eaee..c0f4a265 100644
--- a/components/ContinueWatchingPoster.tsx
+++ b/components/ContinueWatchingPoster.tsx
@@ -6,6 +6,7 @@ import type React from "react";
import { useMemo } from "react";
import { View } from "react-native";
import { apiAtom } from "@/providers/JellyfinProvider";
+import { ProgressBar } from "./common/ProgressBar";
import { WatchedIndicator } from "./WatchedIndicator";
type ContinueWatchingPosterProps = {
@@ -62,18 +63,6 @@ const ContinueWatchingPoster: React.FC = ({
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
}, [item]);
- const progress = useMemo(() => {
- if (item.Type === "Program") {
- const startDate = new Date(item.StartDate || "");
- const endDate = new Date(item.EndDate || "");
- const now = new Date();
- const total = endDate.getTime() - startDate.getTime();
- const elapsed = now.getTime() - startDate.getTime();
- return (elapsed / total) * 100;
- }
- return item.UserData?.PlayedPercentage || 0;
- }, [item]);
-
if (!url)
return ;
@@ -101,22 +90,8 @@ const ContinueWatchingPoster: React.FC = ({
)}
- {!progress && }
- {progress > 0 && (
- <>
-
-
- >
- )}
+ {!item.UserData?.Played && }
+
);
};
diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx
index 552484eb..990f9f0d 100644
--- a/components/DownloadItem.tsx
+++ b/components/DownloadItem.tsx
@@ -14,16 +14,14 @@ import { t } from "i18next";
import { useAtom } from "jotai";
import type React from "react";
import { useCallback, useMemo, useRef, useState } from "react";
-import { Alert, Platform, View, type ViewProps } from "react-native";
+import { Alert, Platform, Switch, View, type ViewProps } from "react-native";
import { toast } from "sonner-native";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueAtom } from "@/utils/atoms/queue";
-import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
+import { useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
-import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
-import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
-import download from "@/utils/profiles/download";
+import { getDownloadUrl } from "@/utils/jellyfin/media/getDownloadUrl";
import { AudioTrackSelector } from "./AudioTrackSelector";
import { type Bitrate, BitrateSelector } from "./BitrateSelector";
import { Button } from "./Button";
@@ -54,11 +52,13 @@ export const DownloadItems: React.FC = ({
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
- const [queue, setQueue] = useAtom(queueAtom);
+ const [queue, _setQueue] = useAtom(queueAtom);
const [settings] = useSettings();
+ const [downloadUnwatchedOnly, setDownloadUnwatchedOnly] = useState(false);
- const { processes, startBackgroundDownload, downloadedFiles } = useDownload();
- //const { startRemuxing } = useRemuxHlsToMp4();
+ const { processes, startBackgroundDownload, getDownloadedItems } =
+ useDownload();
+ const downloadedFiles = getDownloadedItems();
const [selectedMediaSource, setSelectedMediaSource] = useState<
MediaSourceInfo | undefined | null
@@ -77,10 +77,6 @@ export const DownloadItems: React.FC = ({
() => user?.Policy?.EnableContentDownloading,
[user],
);
- const usingOptimizedServer = useMemo(
- () => settings?.downloadMethod === DownloadMethod.Optimized,
- [settings],
- );
const bottomSheetModalRef = useRef(null);
@@ -102,6 +98,13 @@ export const DownloadItems: React.FC = ({
[items, downloadedFiles],
);
+ const itemsToDownload = useMemo(() => {
+ if (downloadUnwatchedOnly) {
+ return itemsNotDownloaded.filter((item) => !item.UserData?.Played);
+ }
+ return itemsNotDownloaded;
+ }, [itemsNotDownloaded, downloadUnwatchedOnly]);
+
const allItemsDownloaded = useMemo(() => {
if (items.length === 0) return false;
return itemsNotDownloaded.length === 0;
@@ -144,31 +147,6 @@ export const DownloadItems: React.FC = ({
);
};
- const acceptDownloadOptions = useCallback(() => {
- if (userCanDownload === true) {
- if (itemsNotDownloaded.some((i) => !i.Id)) {
- throw new Error("No item id");
- }
- closeModal();
-
- initiateDownload(...itemsNotDownloaded);
- } else {
- toast.error(
- t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
- );
- }
- }, [
- queue,
- setQueue,
- itemsNotDownloaded,
- usingOptimizedServer,
- userCanDownload,
- maxBitrate,
- selectedMediaSource,
- selectedAudioStream,
- selectedSubtitleStream,
- ]);
-
const initiateDownload = useCallback(
async (...items: BaseItemDto[]) => {
if (
@@ -181,46 +159,53 @@ export const DownloadItems: React.FC = ({
"DownloadItem ~ initiateDownload: No api or user or item",
);
}
- let mediaSource = selectedMediaSource;
- let audioIndex: number | undefined = selectedAudioStream;
- let subtitleIndex: number | undefined = selectedSubtitleStream;
+ const downloadDetailsPromises = items.map(async (item) => {
+ const { mediaSource, audioIndex, subtitleIndex } =
+ itemsNotDownloaded.length > 1
+ ? getDefaultPlaySettings(item, settings!)
+ : {
+ mediaSource: selectedMediaSource,
+ audioIndex: selectedAudioStream,
+ subtitleIndex: selectedSubtitleStream,
+ };
- for (const item of items) {
- if (itemsNotDownloaded.length > 1) {
- const defaults = getDefaultPlaySettings(item, settings!);
- mediaSource = defaults.mediaSource;
- audioIndex = defaults.audioIndex;
- subtitleIndex = defaults.subtitleIndex;
- }
-
- const res = await getStreamUrl({
+ const downloadDetails = await getDownloadUrl({
api,
item,
- startTimeTicks: 0,
- userId: user?.Id,
- audioStreamIndex: audioIndex,
- maxStreamingBitrate: maxBitrate.value,
- mediaSourceId: mediaSource?.Id,
- subtitleStreamIndex: subtitleIndex,
- deviceProfile: download,
- download: true,
- // deviceId: mediaSource?.Id,
+ userId: user.Id!,
+ mediaSource: mediaSource!,
+ audioStreamIndex: audioIndex ?? -1,
+ subtitleStreamIndex: subtitleIndex ?? -1,
+ maxBitrate,
+ deviceId: api.deviceInfo.id,
});
- if (!res) {
+ return {
+ url: downloadDetails?.url,
+ item,
+ mediaSource: downloadDetails?.mediaSource,
+ };
+ });
+
+ const downloadDetails = await Promise.all(downloadDetailsPromises);
+ for (const { url, item, mediaSource } of downloadDetails) {
+ if (!url) {
Alert.alert(
t("home.downloads.something_went_wrong"),
t("home.downloads.could_not_get_stream_url_from_jellyfin"),
);
continue;
}
-
- const { mediaSource: source, url } = res;
-
- if (!url || !source) throw new Error("No url");
-
- saveDownloadItemInfoToDiskTmp(item, source, url);
- await startBackgroundDownload(url, item, source, maxBitrate);
+ if (!mediaSource) {
+ console.error(`Could not get download URL for ${item.Name}`);
+ toast.error(
+ t("Could not get download URL for {{itemName}}", {
+ itemName: item.Name,
+ }),
+ );
+ continue;
+ }
+ await startBackgroundDownload(url, item, mediaSource, maxBitrate);
}
},
[
@@ -232,11 +217,25 @@ export const DownloadItems: React.FC = ({
selectedSubtitleStream,
settings,
maxBitrate,
- usingOptimizedServer,
startBackgroundDownload,
],
);
+ const acceptDownloadOptions = useCallback(() => {
+ if (userCanDownload === true) {
+ if (itemsToDownload.some((i) => !i.Id)) {
+ throw new Error("No item id");
+ }
+ closeModal();
+
+ initiateDownload(...itemsToDownload);
+ } else {
+ toast.error(
+ t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
+ );
+ }
+ }, [closeModal, initiateDownload, itemsToDownload, userCanDownload]);
+
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
= ({
if (itemsNotDownloaded.length !== 1) return;
const { bitrate, mediaSource, audioIndex, subtitleIndex } =
getDefaultPlaySettings(items[0], settings);
-
setSelectedMediaSource(mediaSource ?? undefined);
setSelectedAudioStream(audioIndex ?? 0);
setSelectedSubtitleStream(subtitleIndex ?? -1);
@@ -327,7 +325,7 @@ export const DownloadItems: React.FC = ({
{subtitle ||
t("item_card.download.download_x_item", {
- item_count: itemsNotDownloaded.length,
+ item_count: itemsToDownload.length,
})}
@@ -337,6 +335,15 @@ export const DownloadItems: React.FC = ({
onChange={setMaxBitrate}
selected={maxBitrate}
/>
+ {itemsNotDownloaded.length > 1 && (
+
+ {t("item_card.download.download_unwatched_only")}
+
+
+ )}
{itemsNotDownloaded.length === 1 && (
<>
= ({
>
)}
+
-
-
- {usingOptimizedServer
- ? t("item_card.download.using_optimized_server")
- : t("item_card.download.using_default_method")}
-
-
diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx
index d7a5d598..83c303f7 100644
--- a/components/ItemContent.tsx
+++ b/components/ItemContent.tsx
@@ -45,8 +45,13 @@ export type SelectedOptions = {
subtitleIndex: number;
};
-export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
- ({ item }) => {
+interface ItemContentProps {
+ item: BaseItemDto;
+ isOffline: boolean;
+}
+
+export const ItemContent: React.FC = React.memo(
+ ({ item, isOffline }) => {
const [api] = useAtom(apiAtom);
const [settings] = useSettings();
const { orientation } = useOrientation();
@@ -68,7 +73,16 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
defaultBitrate,
defaultMediaSource,
defaultSubtitleIndex,
- } = useDefaultPlaySettings(item, settings);
+ } = useDefaultPlaySettings(item!, settings);
+
+ const logoUrl = useMemo(
+ () => (item ? getLogoImageUrlById({ api, item }) : null),
+ [api, item],
+ );
+
+ const loading = useMemo(() => {
+ return Boolean(logoUrl && loadingLogo);
+ }, [loadingLogo, logoUrl]);
// Needs to automatically change the selected to the default values for default indexes.
useEffect(() => {
@@ -116,22 +130,15 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
}, [item, navigation, user]);
useEffect(() => {
- if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)
- setHeaderHeight(230);
- else if (item.Type === "Movie") setHeaderHeight(500);
- else setHeaderHeight(350);
- }, [item.Type, orientation]);
+ if (item) {
+ if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)
+ setHeaderHeight(230);
+ else if (item.Type === "Movie") setHeaderHeight(500);
+ else setHeaderHeight(350);
+ }
+ }, [item, orientation]);
- const logoUrl = useMemo(
- () => getLogoImageUrlById({ api, item }),
- [api, item],
- );
-
- const loading = useMemo(() => {
- return Boolean(logoUrl && loadingLogo);
- }, [loadingLogo, logoUrl]);
-
- if (!selectedOptions) return ;
+ if (!item || !selectedOptions) return null;
return (
= React.memo(
- {item.Type !== "Program" && !Platform.isTV && (
+ {item.Type !== "Program" && !Platform.isTV && !isOffline && (
= React.memo(
className='grow'
selectedOptions={selectedOptions}
item={item}
+ isOffline={isOffline}
/>
{item.Type === "Episode" && (
-
+
)}
-
+ {!isOffline && (
+
+ )}
{item.Type !== "Program" && (
<>
- {item.Type === "Episode" && (
+ {item.Type === "Episode" && !isOffline && (
)}
-
+ {!isOffline && (
+
+ )}
- {item.People && item.People.length > 0 && (
+ {item.People && item.People.length > 0 && !isOffline && (
{item.People.slice(0, 3).map((person, idx) => (
= React.memo(
)}
-
+ {!isOffline && }
>
)}
diff --git a/components/ItemHeader.tsx b/components/ItemHeader.tsx
index b9e3006d..51cf4d0a 100644
--- a/components/ItemHeader.tsx
+++ b/components/ItemHeader.tsx
@@ -33,16 +33,16 @@ export const ItemHeader: React.FC = ({ item, ...props }) => {
{item.Type === "Episode" && (
- <>
+
- >
+
)}
{item.Type === "Movie" && (
- <>
+
- >
+
)}
diff --git a/components/ItemTechnicalDetails.tsx b/components/ItemTechnicalDetails.tsx
index 81c04e0a..c0d5e54d 100644
--- a/components/ItemTechnicalDetails.tsx
+++ b/components/ItemTechnicalDetails.tsx
@@ -236,6 +236,7 @@ const formatFileSize = (bytes?: number | null) => {
if (bytes === 0) return "0 Byte";
const i = Number.parseInt(
Math.floor(Math.log(bytes) / Math.log(1024)).toString(),
+ 10,
);
return `${Math.round((bytes / 1024 ** i) * 100) / 100} ${sizes[i]}`;
};
diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx
index e6ba6218..e013a826 100644
--- a/components/PlayButton.tsx
+++ b/components/PlayButton.tsx
@@ -38,6 +38,7 @@ import type { SelectedOptions } from "./ItemContent";
interface Props extends React.ComponentProps {
item: BaseItemDto;
selectedOptions: SelectedOptions;
+ isOffline?: boolean;
}
const ANIMATION_DURATION = 500;
@@ -46,6 +47,7 @@ const MIN_PLAYBACK_WIDTH = 15;
export const PlayButton: React.FC = ({
item,
selectedOptions,
+ isOffline,
...props
}: Props) => {
const { showActionSheetWithOptions } = useActionSheet();
@@ -75,7 +77,7 @@ export const PlayButton: React.FC = ({
}
router.push(`/player/direct-player?${q}`);
},
- [router],
+ [router, isOffline],
);
const onPress = useCallback(async () => {
@@ -90,6 +92,8 @@ export const PlayButton: React.FC = ({
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
+ playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
+ offline: isOffline ? "true" : "false",
});
const queryString = queryParams.toString();
diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx
index cab1e34f..e055b8cd 100644
--- a/components/PlayedStatus.tsx
+++ b/components/PlayedStatus.tsx
@@ -1,5 +1,4 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
-import { useQueryClient } from "@tanstack/react-query";
import type React from "react";
import { View, type ViewProps } from "react-native";
import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
@@ -7,44 +6,13 @@ import { RoundButton } from "./RoundButton";
interface Props extends ViewProps {
items: BaseItemDto[];
+ isOffline?: boolean;
size?: "default" | "large";
}
export const PlayedStatus: React.FC = ({ items, ...props }) => {
- const queryClient = useQueryClient();
-
- const _invalidateQueries = () => {
- items.forEach((item) => {
- queryClient.invalidateQueries({
- queryKey: ["item", item.Id],
- });
- });
- queryClient.invalidateQueries({
- queryKey: ["resumeItems"],
- });
- queryClient.invalidateQueries({
- queryKey: ["continueWatching"],
- });
- queryClient.invalidateQueries({
- queryKey: ["nextUp-all"],
- });
- queryClient.invalidateQueries({
- queryKey: ["nextUp"],
- });
- queryClient.invalidateQueries({
- queryKey: ["episodes"],
- });
- queryClient.invalidateQueries({
- queryKey: ["seasons"],
- });
- queryClient.invalidateQueries({
- queryKey: ["home"],
- });
- };
-
const allPlayed = items.every((item) => item.UserData?.Played);
-
- const markAsPlayedStatus = useMarkAsPlayed(items);
+ const toggle = useMarkAsPlayed(items);
return (
@@ -52,8 +20,7 @@ export const PlayedStatus: React.FC = ({ items, ...props }) => {
fillColor={allPlayed ? "primary" : undefined}
icon={allPlayed ? "checkmark" : "checkmark"}
onPress={async () => {
- console.log(allPlayed);
- await markAsPlayedStatus(!allPlayed);
+ await toggle(!allPlayed);
}}
size={props.size}
/>
diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx
index 81d3b885..de3f4697 100644
--- a/components/SubtitleTrackSelector.tsx
+++ b/components/SubtitleTrackSelector.tsx
@@ -20,8 +20,7 @@ export const SubtitleTrackSelector: React.FC = ({
selected,
...props
}) => {
- const isTv = Platform.isTV;
-
+ const { t } = useTranslation();
const subtitleStreams = useMemo(() => {
return source?.MediaStreams?.filter((x) => x.Type === "Subtitle");
}, [source]);
@@ -31,10 +30,7 @@ export const SubtitleTrackSelector: React.FC = ({
[subtitleStreams, selected],
);
- const { t } = useTranslation();
-
- if (isTv) return null;
- if (subtitleStreams?.length === 0) return null;
+ if (Platform.isTV || subtitleStreams?.length === 0) return null;
return (
> = ({
onPress={() => {
if (!result) return;
- // @ts-ignore
+ // @ts-expect-error
router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
params: {
diff --git a/components/common/ProgressBar.tsx b/components/common/ProgressBar.tsx
new file mode 100644
index 00000000..1a47327e
--- /dev/null
+++ b/components/common/ProgressBar.tsx
@@ -0,0 +1,47 @@
+import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import React, { useMemo } from "react";
+import { View } from "react-native";
+
+interface ProgressBarProps {
+ item: BaseItemDto;
+}
+
+export const ProgressBar: React.FC = ({ item }) => {
+ const progress = useMemo(() => {
+ if (item.Type === "Program") {
+ if (!item.StartDate || !item.EndDate) {
+ return 0;
+ }
+ const startDate = new Date(item.StartDate);
+ const endDate = new Date(item.EndDate);
+ const now = new Date();
+ const total = endDate.getTime() - startDate.getTime();
+ if (total <= 0) {
+ return 0;
+ }
+ const elapsed = now.getTime() - startDate.getTime();
+ return (elapsed / total) * 100;
+ }
+ return item.UserData?.PlayedPercentage || 0;
+ }, [item]);
+
+ if (progress <= 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx
index a0832304..5c44ab4f 100644
--- a/components/common/TouchableItemRouter.tsx
+++ b/components/common/TouchableItemRouter.tsx
@@ -11,6 +11,7 @@ import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed";
interface Props extends TouchableOpacityProps {
item: BaseItemDto;
+ isOffline?: boolean;
}
export const itemRouter = (
@@ -50,6 +51,7 @@ export const itemRouter = (
export const TouchableItemRouter: React.FC> = ({
item,
+ isOffline = false,
children,
...props
}) => {
@@ -105,7 +107,10 @@ export const TouchableItemRouter: React.FC> = ({
{
- const url = itemRouter(item, from);
+ let url = itemRouter(item, from);
+ if (isOffline) {
+ url += `&offline=true`;
+ }
// @ts-expect-error
router.push(url);
}}
@@ -114,4 +119,6 @@ export const TouchableItemRouter: React.FC> = ({
{children}
);
+
+ return null;
};
diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx
index 08648f94..e95ba2d2 100644
--- a/components/downloads/ActiveDownloads.tsx
+++ b/components/downloads/ActiveDownloads.tsx
@@ -6,7 +6,6 @@ import { t } from "i18next";
import { useMemo } from "react";
import {
ActivityIndicator,
- Platform,
TouchableOpacity,
type TouchableOpacityProps,
View,
@@ -15,18 +14,17 @@ import {
import { toast } from "sonner-native";
import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider";
-import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
+import { JobStatus } from "@/providers/Downloads/types";
import { storage } from "@/utils/mmkv";
-import type { JobStatus } from "@/utils/optimize-server";
import { formatTimeString } from "@/utils/time";
import { Button } from "../Button";
-const BackGroundDownloader = !Platform.isTV
- ? require("@kesha-antonov/react-native-background-downloader")
- : null;
-
interface Props extends ViewProps {}
+const bytesToMB = (bytes: number) => {
+ return bytes / 1024 / 1024;
+};
+
export const ActiveDownloads: React.FC = ({ ...props }) => {
const { processes } = useDownload();
if (processes?.length === 0)
@@ -60,32 +58,18 @@ interface DownloadCardProps extends TouchableOpacityProps {
}
const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
- const { startDownload } = useDownload();
+ const { startDownload, removeProcess } = useDownload();
const router = useRouter();
- const { removeProcess } = useDownload();
- const [settings] = useSettings();
const queryClient = useQueryClient();
const cancelJobMutation = useMutation({
mutationFn: async (id: string) => {
if (!process) throw new Error("No active download");
-
- try {
- const tasks = await BackGroundDownloader.checkForExistingDownloads();
- for (const task of tasks) {
- if (task.id === id) {
- task.stop();
- }
- }
- } finally {
- await removeProcess(id);
- if (settings?.downloadMethod === DownloadMethod.Optimized) {
- await queryClient.refetchQueries({ queryKey: ["jobs"] });
- }
- }
+ removeProcess(id);
},
onSuccess: () => {
toast.success(t("home.downloads.toasts.download_cancelled"));
+ queryClient.invalidateQueries({ queryKey: ["downloads"] });
},
onError: (e) => {
console.error(e);
@@ -94,11 +78,14 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
});
const eta = (p: JobStatus) => {
- if (!p.speed || !p.progress) return null;
+ if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null;
- const length = p?.item?.RunTimeTicks || 0;
- const timeLeft = (length - length * (p.progress / 100)) / p.speed;
- return formatTimeString(timeLeft, "tick");
+ const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0);
+ if (bytesRemaining <= 0) return null;
+
+ const secondsRemaining = bytesRemaining / p.speed;
+
+ return formatTimeString(secondsRemaining, "s");
};
const base64Image = useMemo(() => {
@@ -111,8 +98,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
className='relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden'
{...props}
>
- {(process.status === "optimizing" ||
- process.status === "downloading") && (
+ {process.status === "downloading" && (
{
) : (
{process.progress.toFixed(0)}%
)}
- {process.speed && (
- {process.speed?.toFixed(2)}x
+ {process.speed && process.speed > 0 && (
+
+ {bytesToMB(process.speed).toFixed(2)} MB/s
+
)}
{eta(process) && (
diff --git a/components/downloads/DownloadSize.tsx b/components/downloads/DownloadSize.tsx
index aa1beb37..c6316ff9 100644
--- a/components/downloads/DownloadSize.tsx
+++ b/components/downloads/DownloadSize.tsx
@@ -13,7 +13,8 @@ export const DownloadSize: React.FC = ({
items,
...props
}) => {
- const { downloadedFiles, getDownloadedItemSize } = useDownload();
+ const { getDownloadedItemSize, getDownloadedItems } = useDownload();
+ const downloadedFiles = getDownloadedItems();
const [size, setSize] = useState();
const itemIds = useMemo(() => items.map((i) => i.Id), [items]);
diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx
index 8dc0c7a5..83c0a4e7 100644
--- a/components/downloads/EpisodeCard.tsx
+++ b/components/downloads/EpisodeCard.tsx
@@ -4,18 +4,13 @@ import {
} from "@expo/react-native-action-sheet";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type React from "react";
-import { useCallback, useMemo } from "react";
-import {
- TouchableOpacity,
- type TouchableOpacityProps,
- View,
-} from "react-native";
+import { useCallback } from "react";
+import { type TouchableOpacityProps, View } from "react-native";
import { Text } from "@/components/common/Text";
+import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { DownloadSize } from "@/components/downloads/DownloadSize";
-import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener";
import { useHaptic } from "@/hooks/useHaptic";
import { useDownload } from "@/providers/DownloadProvider";
-import { storage } from "@/utils/mmkv";
import { runtimeTicksToSeconds } from "@/utils/time";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
@@ -25,24 +20,15 @@ interface EpisodeCardProps extends TouchableOpacityProps {
export const EpisodeCard: React.FC = ({ item }) => {
const { deleteFile } = useDownload();
- const { openFile } = useDownloadedFileOpener();
const { showActionSheetWithOptions } = useActionSheet();
const successHapticFeedback = useHaptic("success");
- const _base64Image = useMemo(() => {
- return storage.getString(item.Id!);
- }, [item]);
-
- const handleOpenFile = useCallback(() => {
- openFile(item);
- }, [item, openFile]);
-
/**
* Handles deleting the file with haptic feedback.
*/
const handleDeleteFile = useCallback(() => {
if (item.Id) {
- deleteFile(item.Id);
+ deleteFile(item.Id, "Episode");
successHapticFeedback();
}
}, [deleteFile, item.Id]);
@@ -73,10 +59,10 @@ export const EpisodeCard: React.FC = ({ item }) => {
}, [showActionSheetWithOptions, handleDeleteFile]);
return (
-
@@ -100,7 +86,7 @@ export const EpisodeCard: React.FC = ({ item }) => {
{item.Overview}
-
+
);
};
diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx
index 5a79ae13..c193d562 100644
--- a/components/downloads/MovieCard.tsx
+++ b/components/downloads/MovieCard.tsx
@@ -7,12 +7,12 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image";
import type React from "react";
import { useCallback, useMemo } from "react";
-import { TouchableOpacity, View } from "react-native";
+import { View } from "react-native";
import { DownloadSize } from "@/components/downloads/DownloadSize";
-import { useDownloadedFileOpener } from "@/hooks/useDownloadedFileOpener";
-import { useHaptic } from "@/hooks/useHaptic";
import { useDownload } from "@/providers/DownloadProvider";
import { storage } from "@/utils/mmkv";
+import { ProgressBar } from "../common/ProgressBar";
+import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { ItemCardText } from "../ItemCardText";
interface MovieCardProps {
@@ -26,16 +26,10 @@ interface MovieCardProps {
*/
export const MovieCard: React.FC = ({ item }) => {
const { deleteFile } = useDownload();
- const { openFile } = useDownloadedFileOpener();
const { showActionSheetWithOptions } = useActionSheet();
- const successHapticFeedback = useHaptic("success");
-
- const handleOpenFile = useCallback(() => {
- openFile(item);
- }, [item, openFile]);
const base64Image = useMemo(() => {
- return storage.getString(item.Id!);
+ return storage.getString(item?.Id!);
}, []);
/**
@@ -43,8 +37,7 @@ export const MovieCard: React.FC = ({ item }) => {
*/
const handleDeleteFile = useCallback(() => {
if (item.Id) {
- deleteFile(item.Id);
- successHapticFeedback();
+ deleteFile(item.Id, "Movie");
}
}, [deleteFile, item.Id]);
@@ -74,9 +67,9 @@ export const MovieCard: React.FC = ({ item }) => {
}, [showActionSheetWithOptions, handleDeleteFile]);
return (
-
+
{base64Image ? (
-
+
= ({ item }) => {
resizeMode: "cover",
}}
/>
+
) : (
-
+
+
)}
-
+
);
};
diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx
index bc0b6479..1e300576 100644
--- a/components/home/LargeMovieCarousel.tsx
+++ b/components/home/LargeMovieCarousel.tsx
@@ -154,7 +154,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
if (!from) return;
const url = itemRouter(item, from);
lightHapticFeedback();
- // @ts-ignore
+ // @ts-expect-error
if (url) router.push(url);
}, [item, from]);
diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx
index 483fd156..dc18b464 100644
--- a/components/home/ScrollingCollectionList.tsx
+++ b/components/home/ScrollingCollectionList.tsx
@@ -20,6 +20,7 @@ interface Props extends ViewProps {
queryKey: QueryKey;
queryFn: QueryFunction;
hideIfEmpty?: boolean;
+ isOffline?: boolean;
}
export const ScrollingCollectionList: React.FC = ({
@@ -29,6 +30,7 @@ export const ScrollingCollectionList: React.FC = ({
queryFn,
queryKey,
hideIfEmpty = false,
+ isOffline = false,
...props
}) => {
const { data, isLoading } = useQuery({
@@ -90,6 +92,7 @@ export const ScrollingCollectionList: React.FC = ({
({
}
nestedScrollEnabled
showsVerticalScrollIndicator={false}
- //@ts-ignore
+ //@ts-expect-error
renderItem={({ item, index }) => renderItem(item, index)}
keyExtractor={keyExtractor}
numColumns={3}
diff --git a/components/jellyseerr/discover/Slide.tsx b/components/jellyseerr/discover/Slide.tsx
index 5edb49f5..7352fb6c 100644
--- a/components/jellyseerr/discover/Slide.tsx
+++ b/components/jellyseerr/discover/Slide.tsx
@@ -49,7 +49,7 @@ const Slide = ({
data={data}
onEndReachedThreshold={1}
onEndReached={onEndReached}
- //@ts-ignore
+ //@ts-expect-error
renderItem={({ item, index }) =>
item ? renderItem(item, index) : null
}
diff --git a/components/search/SearchItemWrapper.tsx b/components/search/SearchItemWrapper.tsx
index 0db091c0..91744c98 100644
--- a/components/search/SearchItemWrapper.tsx
+++ b/components/search/SearchItemWrapper.tsx
@@ -35,11 +35,11 @@ export const SearchItemWrapper = ({
showsHorizontalScrollIndicator={false}
keyExtractor={(_, index) => index.toString()}
estimatedItemSize={250}
- /*@ts-ignore */
+ /*@ts-expect-error */
data={items}
onEndReachedThreshold={1}
onEndReached={onEndReached}
- //@ts-ignore
+ //@ts-expect-error
renderItem={({ item }) => (item ? renderItem(item) : null)}
/>
>
diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx
index 60fb9012..414453b9 100644
--- a/components/series/CastAndCrew.tsx
+++ b/components/series/CastAndCrew.tsx
@@ -55,7 +55,7 @@ export const CastAndCrew: React.FC = ({ item, loading, ...props }) => {
{
const url = itemRouter(i, from);
- // @ts-ignore
+ // @ts-expect-error
router.push(url);
}}
className='flex flex-col w-28'
diff --git a/components/series/EpisodeTitleHeader.tsx b/components/series/EpisodeTitleHeader.tsx
index 7f7aa154..c0e7f450 100644
--- a/components/series/EpisodeTitleHeader.tsx
+++ b/components/series/EpisodeTitleHeader.tsx
@@ -19,7 +19,7 @@ export const EpisodeTitleHeader: React.FC = ({ item, ...props }) => {
{
router.push(
- // @ts-ignore
+ // @ts-expect-error
`/(auth)/series/${item.SeriesId}?seasonIndex=${item?.ParentIndexNumber}`,
);
}}
diff --git a/components/series/SeasonDropdown.tsx b/components/series/SeasonDropdown.tsx
index 6cfef3c2..e9b4f0e7 100644
--- a/components/series/SeasonDropdown.tsx
+++ b/components/series/SeasonDropdown.tsx
@@ -94,7 +94,6 @@ export const SeasonDropdown: React.FC = ({
item[keys.id],
initialSeasonIndex,
keys,
- onSelect,
]);
const sortByIndex = (a: BaseItemDto, b: BaseItemDto) =>
@@ -123,16 +122,18 @@ export const SeasonDropdown: React.FC = ({
sideOffset={8}
>
{t("item_card.seasons")}
- {seasons?.sort(sortByIndex).map((season: any) => (
- onSelect(season)}
- >
-
- {season[keys.title]}
-
-
- ))}
+ {seasons?.sort(sortByIndex).map((season: any) => {
+ const title =
+ season[keys.title] || season.Name || `Season ${season.IndexNumber}`;
+ return (
+ onSelect(season)}
+ >
+ {title}
+
+ );
+ })}
);
diff --git a/components/series/SeasonEpisodesCarousel.tsx b/components/series/SeasonEpisodesCarousel.tsx
index d731d145..5a3304ad 100644
--- a/components/series/SeasonEpisodesCarousel.tsx
+++ b/components/series/SeasonEpisodesCarousel.tsx
@@ -1,9 +1,11 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
+import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo, useRef } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
+import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
@@ -16,15 +18,19 @@ import { ItemCardText } from "../ItemCardText";
interface Props extends ViewProps {
item?: BaseItemDto | null;
loading?: boolean;
+ isOffline?: boolean;
}
export const SeasonEpisodesCarousel: React.FC = ({
item,
loading,
+ isOffline,
...props
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
+ const { getDownloadedItems } = useDownload();
+ const downloadedFiles = getDownloadedItems();
const scrollRef = useRef(null);
@@ -41,24 +47,28 @@ export const SeasonEpisodesCarousel: React.FC = ({
isLoading,
isFetching,
} = useQuery({
- queryKey: ["episodes", seasonId],
+ queryKey: ["episodes", seasonId, isOffline],
queryFn: async () => {
- if (!api || !user?.Id) return [];
- const response = await api.axiosInstance.get(
- `${api.basePath}/Shows/${item?.Id}/Episodes`,
- {
- params: {
- userId: user?.Id,
- seasonId,
- Fields:
- "ItemCounts,PrimaryImageAspectRatio,CanDelete,MediaSourceCount,Overview",
- },
- headers: {
- Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
- },
- },
- );
-
+ if (isOffline) {
+ return downloadedFiles
+ ?.filter(
+ (f) => f.item.Type === "Episode" && f.item.SeasonId === seasonId,
+ )
+ .map((f) => f.item);
+ }
+ if (!api || !user?.Id || !item?.SeriesId) return [];
+ const response = await getTvShowsApi(api).getEpisodes({
+ userId: user.Id,
+ seasonId: seasonId || undefined,
+ seriesId: item.SeriesId,
+ fields: [
+ "ItemCounts",
+ "PrimaryImageAspectRatio",
+ "CanDelete",
+ "MediaSourceCount",
+ "Overview",
+ ],
+ });
return response.data.Items as BaseItemDto[];
},
enabled: !!api && !!user?.Id && !!seasonId,
diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx
index 7f50fa4d..f617ae8b 100644
--- a/components/series/SeasonPicker.tsx
+++ b/components/series/SeasonPicker.tsx
@@ -86,7 +86,8 @@ export const SeasonPicker: React.FC = ({ item }) => {
userId: user.Id,
seasonId: selectedSeasonId,
enableUserData: true,
- fields: ["MediaSources", "MediaStreams", "Overview"],
+ // Note: Including trick play is necessary to enable trick play downloads
+ fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
});
if (res.data.TotalRecordCount === 0)
@@ -97,6 +98,10 @@ export const SeasonPicker: React.FC = ({ item }) => {
return res.data.Items;
},
+ select: (data) =>
+ [...(data || [])].sort(
+ (a, b) => (a.IndexNumber ?? 0) - (b.IndexNumber ?? 0),
+ ),
enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId,
});
diff --git a/components/settings/DownloadSettings.tsx b/components/settings/DownloadSettings.tsx
index dc4bb669..aed633e7 100644
--- a/components/settings/DownloadSettings.tsx
+++ b/components/settings/DownloadSettings.tsx
@@ -1,34 +1,17 @@
-import { Ionicons } from "@expo/vector-icons";
-import { useQueryClient } from "@tanstack/react-query";
-import { useRouter } from "expo-router";
import { useMemo } from "react";
-import { Platform, Switch, TouchableOpacity } from "react-native";
-import { Stepper } from "@/components/inputs/Stepper";
-import { useDownload } from "@/providers/DownloadProvider";
-import {
- DownloadMethod,
- type Settings,
- useSettings,
-} from "@/utils/atoms/settings";
-
-const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
-
import { useTranslation } from "react-i18next";
+import { Stepper } from "@/components/inputs/Stepper";
import DisabledSetting from "@/components/settings/DisabledSetting";
-import { Text } from "../common/Text";
+import { type Settings, useSettings } from "@/utils/atoms/settings";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
export default function DownloadSettings({ ...props }) {
const [settings, updateSettings, pluginSettings] = useSettings();
- const { setProcesses } = useDownload();
- const router = useRouter();
- const queryClient = useQueryClient();
const { t } = useTranslation();
const allDisabled = useMemo(
() =>
- pluginSettings?.downloadMethod?.locked === true &&
pluginSettings?.remuxConcurrentLimit?.locked === true &&
pluginSettings?.autoDownload.locked === true,
[pluginSettings],
@@ -39,70 +22,9 @@ export default function DownloadSettings({ ...props }) {
return (
-
-
-
-
-
- {settings.downloadMethod === DownloadMethod.Remux
- ? t("home.settings.downloads.default")
- : t("home.settings.downloads.optimized")}
-
-
-
-
-
-
- {t("home.settings.downloads.download_method")}
-
- {
- updateSettings({ downloadMethod: DownloadMethod.Remux });
- setProcesses([]);
- }}
- >
-
- {t("home.settings.downloads.default")}
-
-
- {
- updateSettings({ downloadMethod: DownloadMethod.Optimized });
- setProcesses([]);
- queryClient.invalidateQueries({ queryKey: ["search"] });
- }}
- >
-
- {t("home.settings.downloads.optimized")}
-
-
-
-
-
-
-
-
- updateSettings({ autoDownload: value })}
- />
-
-
- router.push("/settings/optimized-server/page")}
- showArrow
- title={t("home.settings.downloads.optimized_versions_server")}
- />
);
diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx
index bef4a6ae..80543e51 100644
--- a/components/settings/HomeIndex.tsx
+++ b/components/settings/HomeIndex.tsx
@@ -82,6 +82,17 @@ export const HomeIndex = () => {
const scrollViewRef = useRef(null);
const { downloadedFiles, cleanCacheDirectory } = useDownload();
+ const prevIsConnected = useRef(false);
+ const invalidateCache = useInvalidatePlaybackProgressCache();
+ useEffect(() => {
+ // Only invalidate cache when transitioning from offline to online
+ if (isConnected && !prevIsConnected.current) {
+ invalidateCache();
+ }
+ // Update the ref to the current state for the next render
+ prevIsConnected.current = isConnected;
+ }, [isConnected, invalidateCache]);
+
useEffect(() => {
if (Platform.isTV) {
navigation.setOptions({
@@ -144,10 +155,6 @@ export const HomeIndex = () => {
setIsConnected(state.isConnected);
});
- // cleanCacheDirectory().catch((e) =>
- // console.error("Something went wrong cleaning cache directory")
- // );
-
return () => {
unsubscribe();
};
@@ -188,8 +195,6 @@ export const HomeIndex = () => {
);
}, [userViews]);
- const invalidateCache = useInvalidatePlaybackProgressCache();
-
const refetch = async () => {
setLoading(true);
await refreshStreamyfinPluginSettings();
diff --git a/components/settings/OptimizedServerForm.tsx b/components/settings/OptimizedServerForm.tsx
deleted file mode 100644
index f4f75791..00000000
--- a/components/settings/OptimizedServerForm.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { Linking, TextInput, View } from "react-native";
-import { Text } from "../common/Text";
-
-interface Props {
- value: string;
- onChangeValue: (value: string) => void;
-}
-
-export const OptimizedServerForm: React.FC = ({
- value,
- onChangeValue,
-}) => {
- const handleOpenLink = () => {
- Linking.openURL("https://github.com/streamyfin/optimized-versions-server");
- };
-
- const { t } = useTranslation();
-
- return (
-
-
-
- {t("home.settings.downloads.url")}
- onChangeValue(text)}
- />
-
-
-
- {t("home.settings.downloads.optimized_version_hint")}{" "}
-
- {t("home.settings.downloads.read_more_about_optimized_server")}
-
-
-
- );
-};
diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx
index 5e3c278e..02c2ed0a 100644
--- a/components/settings/StorageSettings.tsx
+++ b/components/settings/StorageSettings.tsx
@@ -17,9 +17,9 @@ export const StorageSettings = () => {
const errorHapticFeedback = useHaptic("error");
const { data: size } = useQuery({
- queryKey: ["appSize", appSizeUsage],
+ queryKey: ["appSize"],
queryFn: async () => {
- const app = await appSizeUsage;
+ const app = await appSizeUsage();
const remaining = await FileSystem.getFreeDiskStorageAsync();
const total = await FileSystem.getTotalDiskCapacityAsync();
@@ -58,7 +58,7 @@ export const StorageSettings = () => {
{size && (
- <>
+
{
/>
- >
+
)}
{size && (
- <>
+
@@ -99,7 +96,7 @@ export const StorageSettings = () => {
})}
- >
+
)}
diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx
index 4b33ad6b..b9a1ec80 100644
--- a/components/video-player/controls/Controls.tsx
+++ b/components/video-player/controls/Controls.tsx
@@ -35,10 +35,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { Loader } from "@/components/Loader";
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
-import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
import { useHaptic } from "@/hooks/useHaptic";
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
+import { usePlaybackManager } from "@/hooks/usePlaybackManager";
import { useTrickplay } from "@/hooks/useTrickplay";
import type { TrackInfo, VlcPlayerViewRef } from "@/modules/VlcPlayer.types";
import { apiAtom } from "@/providers/JellyfinProvider";
@@ -82,8 +82,8 @@ interface Props {
isVideoLoaded?: boolean;
mediaSource?: MediaSourceInfo | null;
seek: (ticks: number) => void;
- startPictureInPicture: () => Promise;
- play: (() => Promise) | (() => void);
+ startPictureInPicture?: () => Promise;
+ play: () => void;
pause: () => void;
getAudioTracks?: (() => Promise) | (() => TrackInfo[]);
getSubtitleTracks?: (() => Promise) | (() => TrackInfo[]);
@@ -119,7 +119,6 @@ export const Controls: FC = ({
setSubtitleTrack,
setAudioTrack,
offline = false,
- enableTrickplay = true,
isVlc = false,
}) => {
const [settings, updateSettings] = useSettings();
@@ -134,13 +133,17 @@ export const Controls: FC = ({
const [showAudioSlider, setShowAudioSlider] = useState(false);
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
- const { previousItem, nextItem } = useAdjacentItems({ item });
+ const { previousItem, nextItem } = usePlaybackManager({
+ item,
+ isOffline: offline,
+ });
+
const {
trickPlayUrl,
calculateTrickplayUrl,
trickplayInfo,
prefetchAllTrickplayImages,
- } = useTrickplay(item, !offline && enableTrickplay);
+ } = useTrickplay(item);
const [currentTime, setCurrentTime] = useState(0);
const [remainingTime, setRemainingTime] = useState(Number.POSITIVE_INFINITY);
@@ -303,19 +306,21 @@ export const Controls: FC = ({
}>();
const { showSkipButton, skipIntro } = useIntroSkipper(
- offline ? undefined : item.Id,
+ item?.Id!,
currentTime,
seek,
play,
isVlc,
+ offline,
);
const { showSkipCreditButton, skipCredit } = useCreditSkipper(
- offline ? undefined : item.Id,
+ item?.Id!,
currentTime,
seek,
play,
isVlc,
+ offline,
);
const goToItemCommon = useCallback(
@@ -323,14 +328,12 @@ export const Controls: FC = ({
if (!item || !settings) {
return;
}
-
lightHapticFeedback();
-
const previousIndexes = {
subtitleIndex: subtitleIndex
- ? Number.parseInt(subtitleIndex)
+ ? Number.parseInt(subtitleIndex, 10)
: undefined,
- audioIndex: audioIndex ? Number.parseInt(audioIndex) : undefined,
+ audioIndex: audioIndex ? Number.parseInt(audioIndex, 10) : undefined,
};
const {
@@ -343,15 +346,18 @@ export const Controls: FC = ({
previousIndexes,
mediaSource ?? undefined,
);
-
const queryParams = new URLSearchParams({
itemId: item.Id ?? "",
audioIndex: defaultAudioIndex?.toString() ?? "",
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
mediaSourceId: newMediaSource?.Id ?? "",
bitrateValue: bitrateValue?.toString(),
+ playbackPosition:
+ item.UserData?.PlaybackPositionTicks?.toString() ?? "",
}).toString();
+ console.log("queryParams", queryParams);
+
// @ts-expect-error
router.replace(`player/direct-player?${queryParams}`);
},
@@ -434,10 +440,18 @@ export const Controls: FC = ({
const goToItem = useCallback(
async (itemId: string) => {
- const gotoItem = await getItemById(api, itemId);
- if (!gotoItem) {
+ if (offline) {
+ const queryParams = new URLSearchParams({
+ itemId: itemId,
+ playbackPosition:
+ item.UserData?.PlaybackPositionTicks?.toString() ?? "",
+ }).toString();
+ // @ts-expect-error
+ router.replace(`player/direct-player?${queryParams}`);
return;
}
+ const gotoItem = await getItemById(api, itemId);
+ if (!gotoItem) return;
goToItemCommon(gotoItem);
},
[goToItemCommon, api],
@@ -726,8 +740,8 @@ export const Controls: FC = ({
pointerEvents={showControls ? "auto" : "none"}
className={"flex flex-row w-full pt-2"}
>
- {!Platform.isTV && (
-
+
+ {!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && (
= ({
>
-
- )}
+ )}
+
{!Platform.isTV &&
- settings.defaultPlayer === VideoPlayer.VLC_4 && (
+ (settings.defaultPlayer === VideoPlayer.VLC_4 ||
+ Platform.OS === "android") && (
= ({
/>
)}
-
- {item?.Type === "Episode" && !offline && (
+ {item?.Type === "Episode" && (
{
switchOnEpisodeMode();
@@ -766,7 +780,7 @@ export const Controls: FC = ({
)}
- {previousItem && !offline && (
+ {previousItem && (
= ({
)}
-
- {nextItem && !offline && (
+ {nextItem && (
goToNextItem({ isAutoPlay: false })}
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
@@ -783,7 +796,6 @@ export const Controls: FC = ({
)}
-
{/* {mediaSource?.TranscodingUrl && ( */}
= ({
color='white'
/>
- {/* )} */}
= ({
-
({});
export const EpisodeList: React.FC = ({ item, close, goToItem }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
- const _insets = useSafeAreaInsets(); // Get safe area insets
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
const scrollViewRef = useRef(null); // Reference to the HorizontalScroll
const scrollToIndex = (index: number) => {
scrollViewRef.current?.scrollToIndex(index, 100);
};
+ const { offline } = useGlobalSearchParams<{
+ offline: string;
+ }>();
+ const isOffline = offline === "true";
// Set the initial season index
useEffect(() => {
if (item.SeriesId) {
setSeasonIndexState((prev) => ({
...prev,
- [item.SeriesId ?? ""]: item.ParentIndexNumber ?? 0,
+ [item.ParentId ?? ""]: item.ParentIndexNumber ?? 0,
}));
}
}, []);
- const seasonIndex = seasonIndexState[item.SeriesId ?? ""];
- const [seriesItem, setSeriesItem] = useState(null);
+ const { getDownloadedItems } = useDownload();
+ const downloadedFiles = getDownloadedItems();
- // This effect fetches the series item data/
- useEffect(() => {
- if (item.SeriesId) {
- getUserItemData({ api, userId: user?.Id, itemId: item.SeriesId }).then(
- (res) => {
- setSeriesItem(res);
- },
- );
- }
- }, [item.SeriesId]);
+ const seasonIndex = seasonIndexState[item.ParentId ?? ""];
const { data: seasons } = useQuery({
queryKey: ["seasons", item.SeriesId],
queryFn: async () => {
+ if (isOffline) {
+ if (!item.SeriesId) return [];
+ const seriesEpisodes = downloadedFiles?.filter(
+ (f: DownloadedItem) => f.item.SeriesId === item.SeriesId,
+ );
+ const seasonNumbers = [
+ ...new Set(
+ seriesEpisodes
+ ?.map((f: DownloadedItem) => f.item.ParentIndexNumber)
+ .filter(Boolean),
+ ),
+ ];
+ // Create fake season objects
+ return seasonNumbers.map((seasonNumber) => ({
+ Id: seasonNumber?.toString(),
+ IndexNumber: seasonNumber,
+ Name: `Season ${seasonNumber}`,
+ SeriesId: item.SeriesId,
+ }));
+ }
+
if (!api || !user?.Id || !item.SeriesId) return [];
- const response = await api.axiosInstance.get(
- `${api.basePath}/Shows/${item.SeriesId}/Seasons`,
- {
- params: {
- userId: user?.Id,
- itemId: item.SeriesId,
- Fields:
- "ItemCounts,PrimaryImageAspectRatio,CanDelete,MediaSourceCount",
- },
- headers: {
- Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
- },
- },
- );
+ const response = await getTvShowsApi(api).getSeasons({
+ seriesId: item.SeriesId,
+ userId: user.Id,
+ fields: [
+ "ItemCounts",
+ "PrimaryImageAspectRatio",
+ "CanDelete",
+ "MediaSourceCount",
+ ],
+ });
return response.data.Items;
},
- enabled: !!api && !!user?.Id && !!item.SeasonId,
+ enabled: isOffline
+ ? !!item.SeriesId
+ : !!api && !!user?.Id && !!item.SeasonId,
});
const selectedSeasonId: string | null = useMemo(
() =>
- seasons?.find((season: any) => season.IndexNumber === seasonIndex)?.Id,
+ seasons
+ ?.find((season: any) => season.IndexNumber === seasonIndex)
+ ?.Id?.toString() || null,
[seasons, seasonIndex],
);
- const { data: episodes } = useQuery({
+ const { data: episodes, isLoading: episodesLoading } = useQuery({
queryKey: ["episodes", item.SeriesId, selectedSeasonId],
queryFn: async () => {
+ if (isOffline) {
+ if (!item.SeriesId) return [];
+ return downloadedFiles
+ ?.filter(
+ (f: DownloadedItem) =>
+ f.item.SeriesId === item.SeriesId &&
+ f.item.ParentIndexNumber === seasonIndex,
+ )
+ .map((f: DownloadedItem) => f.item);
+ }
if (!api || !user?.Id || !item.Id || !selectedSeasonId) return [];
const res = await getTvShowsApi(api).getEpisodes({
seriesId: item.SeriesId || "",
@@ -112,7 +139,7 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => {
useEffect(() => {
if (item?.Type === "Episode" && item.Id) {
- const index = episodes?.findIndex((ep) => ep.Id === item.Id);
+ const index = episodes?.findIndex((ep: BaseItemDto) => ep.Id === item.Id);
if (index !== undefined && index !== -1) {
setTimeout(() => {
scrollToIndex(index);
@@ -150,12 +177,8 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => {
}
}, [episodes, item.Id]);
- if (!episodes) {
- return ;
- }
-
return (
- = ({ item, close, goToItem }) => {
width: "100%",
}}
>
-
- {seriesItem && (
+
+ {seasons && seasons.length > 0 && !episodesLoading && episodes && (
{
setSeasonIndexState((prev) => ({
...prev,
- [item.SeriesId ?? ""]: season.IndexNumber,
+ [item.ParentId ?? ""]: season.IndexNumber,
}));
}}
/>
@@ -186,64 +204,72 @@ export const EpisodeList: React.FC = ({ item, close, goToItem }) => {
onPress={async () => {
close();
}}
- className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2'
+ className='aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2 ml-auto'
>
- (
-
- {
- goToItem(_item.Id);
- }}
+ {!episodes || episodesLoading ? (
+
+
+
+ ) : (
+ (
+
-
-
-
- {
+ goToItem(_item.Id);
}}
>
- {_item.Name}
-
-
- {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`}
-
-
- {runtimeTicksToSeconds(_item.RunTimeTicks)}
+
+
+
+
+ {_item.Name}
+
+
+ {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`}
+
+
+ {runtimeTicksToSeconds(_item.RunTimeTicks)}
+
+
+
+ {_item.Overview}
-
-
-
-
- {_item.Overview}
-
-
- )}
- keyExtractor={(e: BaseItemDto) => e.Id ?? ""}
- estimatedItemSize={200}
- showsHorizontalScrollIndicator={false}
- />
-
+ )}
+ keyExtractor={(e: BaseItemDto) => e.Id ?? ""}
+ estimatedItemSize={200}
+ showsHorizontalScrollIndicator={false}
+ />
+ )}
+
);
};
diff --git a/components/video-player/controls/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx
index ca1344d6..2317f36a 100644
--- a/components/video-player/controls/contexts/VideoContext.tsx
+++ b/components/video-player/controls/contexts/VideoContext.tsx
@@ -1,3 +1,4 @@
+import { SubtitleDeliveryMethod } from "@jellyfin/sdk/lib/generated-client";
import { router, useLocalSearchParams } from "expo-router";
import type React from "react";
import {
@@ -9,7 +10,6 @@ import {
useState,
} from "react";
import type { TrackInfo } from "@/modules/VlcPlayer.types";
-import { useSettings, VideoPlayer } from "@/utils/atoms/settings";
import type { Track } from "../types";
import { useControlContext } from "./ControlContext";
@@ -48,7 +48,6 @@ export const VideoProvider: React.FC = ({
}) => {
const [audioTracks, setAudioTracks] = useState