fix: rn downloads

This commit is contained in:
Fredrik Burmester
2025-10-01 15:25:16 +02:00
parent 6fc4c33759
commit 32c01c6f89
12 changed files with 222 additions and 197 deletions

View File

@@ -176,6 +176,45 @@ The modal uses these default styles (can be overridden via options):
4. **Avoid nesting** - Don't show modals from within modals 4. **Avoid nesting** - Don't show modals from within modals
5. **Consider UX** - Only use for important, contextual information that requires user attention 5. **Consider UX** - Only use for important, contextual information that requires user attention
## Using with PlatformDropdown
When using `PlatformDropdown` with option groups, avoid setting a `title` on the `OptionGroup` if you're already passing a `title` prop to `PlatformDropdown`. This prevents nested menu behavior on iOS where users have to click through an extra layer.
```tsx
// Good - No title in option group (title is on PlatformDropdown)
const optionGroups: OptionGroup[] = [
{
options: items.map((item) => ({
type: "radio",
label: item.name,
value: item,
selected: item.id === selected?.id,
onPress: () => onChange(item),
})),
},
];
<PlatformDropdown
groups={optionGroups}
title="Select Item" // Title here
// ...
/>
// Bad - Causes nested menu on iOS
const optionGroups: OptionGroup[] = [
{
title: "Items", // This creates a nested Picker on iOS
options: items.map((item) => ({
type: "radio",
label: item.name,
value: item,
selected: item.id === selected?.id,
onPress: () => onChange(item),
})),
},
];
```
## Troubleshooting ## Troubleshooting
### Modal doesn't appear ### Modal doesn't appear

View File

@@ -1,4 +1,3 @@
import * as FileSystem from "expo-file-system";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import * as Sharing from "expo-sharing"; import * as Sharing from "expo-sharing";
import { useCallback, useEffect, useId, useMemo, useState } from "react"; import { useCallback, useEffect, useId, useMemo, useState } from "react";

View File

@@ -28,13 +28,9 @@ import {
} from "@/utils/log"; } from "@/utils/log";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
// TEMPORARILY DISABLED const BackGroundDownloader = !Platform.isTV
// To re-enable: Move package from "disabledDependencies" to "dependencies" in package.json, ? require("@kesha-antonov/react-native-background-downloader")
// run "bun install", then uncomment the require below and remove the null assignment : null;
// const BackGroundDownloader = !Platform.isTV
// ? require("@kesha-antonov/react-native-background-downloader")
// : null;
const BackGroundDownloader = null;
import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
@@ -42,7 +38,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as BackgroundTask from "expo-background-task"; import * as BackgroundTask from "expo-background-task";
import * as Device from "expo-device"; import * as Device from "expo-device";
import * as FileSystem from "expo-file-system"; import { Paths } from "expo-file-system";
const Notifications = !Platform.isTV ? require("expo-notifications") : null; const Notifications = !Platform.isTV ? require("expo-notifications") : null;
@@ -149,7 +145,7 @@ if (!Platform.isTV) {
const token = getTokenFromStorage(); const token = getTokenFromStorage();
const deviceId = getOrSetDeviceId(); const deviceId = getOrSetDeviceId();
const baseDirectory = FileSystem.documentDirectory; const baseDirectory = Paths.document.uri;
if (!token || !deviceId || !baseDirectory) if (!token || !deviceId || !baseDirectory)
return BackgroundTask.BackgroundTaskResult.Failed; return BackgroundTask.BackgroundTaskResult.Failed;
@@ -426,10 +422,10 @@ function Layout() {
<LogProvider> <LogProvider>
<WebSocketProvider> <WebSocketProvider>
<DownloadProvider> <DownloadProvider>
<BottomSheetModalProvider> <GlobalModalProvider>
<GlobalModalProvider> <BottomSheetModalProvider>
<SystemBars style='light' hidden={false} />
<ThemeProvider value={DarkTheme}> <ThemeProvider value={DarkTheme}>
<SystemBars style='light' hidden={false} />
<Stack initialRouteName='(auth)/(tabs)'> <Stack initialRouteName='(auth)/(tabs)'>
<Stack.Screen <Stack.Screen
name='(auth)/(tabs)' name='(auth)/(tabs)'
@@ -471,10 +467,10 @@ function Layout() {
}} }}
closeButton closeButton
/> />
<GlobalModal />
</ThemeProvider> </ThemeProvider>
<GlobalModal /> </BottomSheetModalProvider>
</GlobalModalProvider> </GlobalModalProvider>
</BottomSheetModalProvider>
</DownloadProvider> </DownloadProvider>
</WebSocketProvider> </WebSocketProvider>
</LogProvider> </LogProvider>

View File

@@ -4,7 +4,6 @@ import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router"; import { useLocalSearchParams, useNavigation } from "expo-router";
import { t } from "i18next"; import { t } from "i18next";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import type React from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { import {
Alert, Alert,
@@ -82,10 +81,10 @@ const Login: React.FC = () => {
onPress={() => { onPress={() => {
removeServer(); removeServer();
}} }}
className='flex flex-row items-center' className='flex flex-row items-center pr-2 pl-1'
> >
<Ionicons name='chevron-back' size={18} color={Colors.primary} /> <Ionicons name='chevron-back' size={18} color={Colors.primary} />
<Text className='ml-2 text-purple-600'> <Text className=' ml-1 text-purple-600'>
{t("login.change_server")} {t("login.change_server")}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>

View File

@@ -34,7 +34,6 @@ export const AudioTrackSelector: React.FC<Props> = ({
const optionGroups: OptionGroup[] = useMemo( const optionGroups: OptionGroup[] = useMemo(
() => [ () => [
{ {
title: "Audio streams",
options: options:
audioStreams?.map((audio, idx) => ({ audioStreams?.map((audio, idx) => ({
type: "radio" as const, type: "radio" as const,
@@ -71,26 +70,19 @@ export const AudioTrackSelector: React.FC<Props> = ({
if (isTv) return null; if (isTv) return null;
return ( return (
<View <PlatformDropdown
className='flex shrink' groups={optionGroups}
style={{ trigger={trigger}
minWidth: 50, title={t("item_card.audio")}
open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}} }}
> bottomSheetConfig={{
<PlatformDropdown enablePanDownToClose: true,
groups={optionGroups} }}
trigger={trigger} />
title={t("item_card.audio")}
open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
</View>
); );
}; };

View File

@@ -79,18 +79,16 @@ export const BitrateSelector: React.FC<Props> = ({
const optionGroups: OptionGroup[] = useMemo( const optionGroups: OptionGroup[] = useMemo(
() => [ () => [
{ {
id: "bitrates",
title: "Bitrates",
options: sorted.map((bitrate) => ({ options: sorted.map((bitrate) => ({
id: bitrate.key,
type: "radio" as const, type: "radio" as const,
groupId: "bitrates",
label: bitrate.key, label: bitrate.key,
value: bitrate,
selected: bitrate.value === selected?.value, selected: bitrate.value === selected?.value,
onPress: () => onChange(bitrate),
})), })),
}, },
], ],
[sorted, selected], [sorted, selected, onChange],
); );
const handleOptionSelect = (optionId: string) => { const handleOptionSelect = (optionId: string) => {
@@ -118,27 +116,19 @@ export const BitrateSelector: React.FC<Props> = ({
if (isTv) return null; if (isTv) return null;
return ( return (
<View <PlatformDropdown
className='flex shrink' groups={optionGroups}
style={{ trigger={trigger}
minWidth: 60, title={t("item_card.quality")}
maxWidth: 200, open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}} }}
> bottomSheetConfig={{
<PlatformDropdown enablePanDownToClose: true,
groups={optionGroups} }}
trigger={trigger} />
title={t("item_card.quality")}
open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
</View>
); );
}; };

View File

@@ -359,16 +359,18 @@ export const DownloadItems: React.FC<DownloadProps> = ({
})} })}
</Text> </Text>
</View> </View>
<View className='flex flex-col space-y-2 w-full items-start'> <View className='flex flex-col space-y-2 w-full'>
<BitrateSelector <View className='items-start'>
inverted <BitrateSelector
onChange={(val) => inverted
setSelectedOptions( onChange={(val) =>
(prev) => prev && { ...prev, bitrate: val }, setSelectedOptions(
) (prev) => prev && { ...prev, bitrate: val },
} )
selected={selectedOptions?.bitrate} }
/> selected={selectedOptions?.bitrate}
/>
</View>
{itemsNotDownloaded.length > 1 && ( {itemsNotDownloaded.length > 1 && (
<View className='flex flex-row items-center justify-between w-full py-2'> <View className='flex flex-row items-center justify-between w-full py-2'>
<Text>{t("item_card.download.download_unwatched_only")}</Text> <Text>{t("item_card.download.download_unwatched_only")}</Text>
@@ -380,21 +382,23 @@ export const DownloadItems: React.FC<DownloadProps> = ({
)} )}
{itemsNotDownloaded.length === 1 && ( {itemsNotDownloaded.length === 1 && (
<View> <View>
<MediaSourceSelector <View className='items-start'>
item={items[0]} <MediaSourceSelector
onChange={(val) => item={items[0]}
setSelectedOptions( onChange={(val) =>
(prev) => setSelectedOptions(
prev && { (prev) =>
...prev, prev && {
mediaSource: val, ...prev,
}, mediaSource: val,
) },
} )
selected={selectedOptions?.mediaSource} }
/> selected={selectedOptions?.mediaSource}
/>
</View>
{selectedOptions?.mediaSource && ( {selectedOptions?.mediaSource && (
<View className='flex flex-col space-y-2'> <View className='flex flex-col space-y-2 items-start'>
<AudioTrackSelector <AudioTrackSelector
source={selectedOptions.mediaSource} source={selectedOptions.mediaSource}
onChange={(val) => { onChange={(val) => {
@@ -427,11 +431,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
)} )}
</View> </View>
<Button <Button onPress={acceptDownloadOptions} color='purple'>
className='mt-auto'
onPress={acceptDownloadOptions}
color='purple'
>
{t("item_card.download.download_button")} {t("item_card.download.download_button")}
</Button> </Button>
</View> </View>

View File

@@ -47,19 +47,17 @@ export const MediaSourceSelector: React.FC<Props> = ({
const optionGroups: OptionGroup[] = useMemo( const optionGroups: OptionGroup[] = useMemo(
() => [ () => [
{ {
id: "media-sources",
title: "Media sources",
options: options:
item.MediaSources?.map((source, idx) => ({ item.MediaSources?.map((source) => ({
id: `${source.Id || idx}`,
type: "radio" as const, type: "radio" as const,
groupId: "media-sources",
label: getDisplayName(source), label: getDisplayName(source),
value: source,
selected: source.Id === selected?.Id, selected: source.Id === selected?.Id,
onPress: () => onChange(source),
})) || [], })) || [],
}, },
], ],
[item.MediaSources, selected, getDisplayName], [item.MediaSources, selected, getDisplayName, onChange],
); );
const handleOptionSelect = (optionId: string) => { const handleOptionSelect = (optionId: string) => {
@@ -87,26 +85,19 @@ export const MediaSourceSelector: React.FC<Props> = ({
if (isTv) return null; if (isTv) return null;
return ( return (
<View <PlatformDropdown
className='flex shrink' groups={optionGroups}
style={{ trigger={trigger}
minWidth: 50, title={t("item_card.video")}
open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}} }}
> bottomSheetConfig={{
<PlatformDropdown enablePanDownToClose: true,
groups={optionGroups} }}
trigger={trigger} />
title={t("item_card.video")}
open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
</View>
); );
}; };

View File

@@ -33,29 +33,27 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
const optionGroups: OptionGroup[] = useMemo(() => { const optionGroups: OptionGroup[] = useMemo(() => {
const options = [ const options = [
{ {
id: "none",
type: "radio" as const, type: "radio" as const,
groupId: "subtitle-streams",
label: t("item_card.none"), label: t("item_card.none"),
value: -1,
selected: selected === -1, selected: selected === -1,
onPress: () => onChange(-1),
}, },
...(subtitleStreams?.map((subtitle, idx) => ({ ...(subtitleStreams?.map((subtitle, idx) => ({
id: `${subtitle.Index || idx}`,
type: "radio" as const, type: "radio" as const,
groupId: "subtitle-streams",
label: subtitle.DisplayTitle || `Subtitle Stream ${idx + 1}`, label: subtitle.DisplayTitle || `Subtitle Stream ${idx + 1}`,
value: subtitle.Index,
selected: subtitle.Index === selected, selected: subtitle.Index === selected,
onPress: () => onChange(subtitle.Index ?? -1),
})) || []), })) || []),
]; ];
return [ return [
{ {
id: "subtitle-streams",
title: "Subtitle tracks",
options, options,
}, },
]; ];
}, [subtitleStreams, selected, t]); }, [subtitleStreams, selected, t, onChange]);
const handleOptionSelect = (optionId: string) => { const handleOptionSelect = (optionId: string) => {
if (optionId === "none") { if (optionId === "none") {
@@ -96,27 +94,19 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
if (Platform.isTV || subtitleStreams?.length === 0) return null; if (Platform.isTV || subtitleStreams?.length === 0) return null;
return ( return (
<View <PlatformDropdown
className='flex col shrink justify-start place-self-start items-start' groups={optionGroups}
style={{ trigger={trigger}
minWidth: 60, title={t("item_card.subtitles")}
maxWidth: 200, open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}} }}
> bottomSheetConfig={{
<PlatformDropdown enablePanDownToClose: true,
groups={optionGroups} }}
trigger={trigger} />
title={t("item_card.subtitles")}
open={open}
onOpenChange={setOpen}
onOptionSelect={handleOptionSelect}
expoUIConfig={{
hostStyle: { flex: 1 },
}}
bottomSheetConfig={{
enablePanDownToClose: true,
}}
/>
</View>
); );
}; };

View File

@@ -1,5 +1,7 @@
// Learn more https://docs.expo.io/guides/customizing-metro // Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config"); const { getDefaultConfig } = require("expo/metro-config");
const path = require("node:path");
const fs = require("node:fs");
/** @type {import('expo/metro-config').MetroConfig} */ /** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef
@@ -23,6 +25,27 @@ if (process.env?.EXPO_TV === "1") {
config.resolver.sourceExts = tvSourceExts; config.resolver.sourceExts = tvSourceExts;
} }
// Support for symlinked packages (yarn link) - only if the directory exists
const linkedPackagePath = path.resolve(
__dirname,
"../react-native-background-downloader",
);
if (fs.existsSync(linkedPackagePath)) {
console.log("Detected symlinked package, configuring Metro to support it");
// Watch the linked package directory
config.watchFolders = [linkedPackagePath];
// Add the parent directory to node module paths
config.resolver.nodeModulesPaths = [path.resolve(__dirname, "node_modules")];
// Map the package to the local directory
config.resolver.extraNodeModules = {
"@kesha-antonov/react-native-background-downloader": linkedPackagePath,
};
}
// config.resolver.unstable_enablePackageExports = false; // config.resolver.unstable_enablePackageExports = false;
module.exports = config; module.exports = config;

View File

@@ -3,12 +3,12 @@ import type {
MediaSourceInfo, MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import * as Application from "expo-application"; import * as Application from "expo-application";
import * as FileSystem from "expo-file-system"; import { Directory, File, Paths } from "expo-file-system";
import * as Notifications from "expo-notifications"; import * as Notifications from "expo-notifications";
import { router } from "expo-router"; import { router } from "expo-router";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { throttle } from "lodash"; import { throttle } from "lodash";
import React, { import {
createContext, createContext,
useCallback, useCallback,
useContext, useContext,
@@ -341,7 +341,10 @@ function useDownloadProvider() {
return api?.accessToken; return api?.accessToken;
}, [api]); }, [api]);
const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`; const APP_CACHE_DOWNLOAD_DIRECTORY = new Directory(
Paths.cache,
`${Application.applicationId}/Downloads/`,
);
const getDownloadsDatabase = (): DownloadsDatabase => { const getDownloadsDatabase = (): DownloadsDatabase => {
const file = storage.getString(DOWNLOADS_DATABASE_KEY); const file = storage.getString(DOWNLOADS_DATABASE_KEY);
@@ -406,20 +409,17 @@ function useDownloadProvider() {
} }
const filename = generateFilename(item); const filename = generateFilename(item);
const trickplayDir = `${FileSystem.documentDirectory}${filename}_trickplay/`; const trickplayDir = new Directory(Paths.document, `${filename}_trickplay`);
await FileSystem.makeDirectoryAsync(trickplayDir, { intermediates: true }); trickplayDir.create({ intermediates: true });
let totalSize = 0; let totalSize = 0;
for (let index = 0; index < trickplayInfo.totalImageSheets; index++) { for (let index = 0; index < trickplayInfo.totalImageSheets; index++) {
const url = generateTrickplayUrl(item, index); const url = generateTrickplayUrl(item, index);
if (!url) continue; if (!url) continue;
const destination = `${trickplayDir}${index}.jpg`; const destination = new File(trickplayDir, `${index}.jpg`);
try { try {
await FileSystem.downloadAsync(url, destination); await File.downloadFileAsync(url, destination);
const fileInfo = await FileSystem.getInfoAsync(destination); totalSize += destination.size;
if (fileInfo.exists) {
totalSize += fileInfo.size;
}
} catch (e) { } catch (e) {
console.error( console.error(
`Failed to download trickplay image ${index} for item ${item.Id}`, `Failed to download trickplay image ${index} for item ${item.Id}`,
@@ -428,7 +428,7 @@ function useDownloadProvider() {
} }
} }
return { path: trickplayDir, size: totalSize }; return { path: trickplayDir.uri, size: totalSize };
}; };
/** /**
@@ -448,9 +448,12 @@ function useDownloadProvider() {
externalSubtitles.map(async (subtitle) => { externalSubtitles.map(async (subtitle) => {
const url = api.basePath + subtitle.DeliveryUrl; const url = api.basePath + subtitle.DeliveryUrl;
const filename = generateFilename(item); const filename = generateFilename(item);
const destination = `${FileSystem.documentDirectory}${filename}_subtitle_${subtitle.Index}`; const destination = new File(
await FileSystem.downloadAsync(url, destination); Paths.document,
subtitle.DeliveryUrl = destination; `${filename}_subtitle_${subtitle.Index}`,
);
await File.downloadFileAsync(url, destination);
subtitle.DeliveryUrl = destination.uri;
}), }),
); );
} }
@@ -544,7 +547,7 @@ function useDownloadProvider() {
}, },
}); });
const filename = generateFilename(process.item); const filename = generateFilename(process.item);
const videoFilePath = `${FileSystem.documentDirectory}${filename}.mp4`; const videoFilePath = new File(Paths.document, `${filename}.mp4`).uri;
BackGroundDownloader?.download({ BackGroundDownloader?.download({
id: process.id, id: process.id,
url: process.inputUrl, url: process.inputUrl,
@@ -596,11 +599,11 @@ function useDownloadProvider() {
) )
.done(async () => { .done(async () => {
const trickPlayData = await downloadTrickplayImages(process.item); const trickPlayData = await downloadTrickplayImages(process.item);
const videoFileInfo = await FileSystem.getInfoAsync(videoFilePath); const videoFile = new File(videoFilePath);
if (!videoFileInfo.exists) { if (!videoFile.exists) {
throw new Error("Downloaded file does not exist"); throw new Error("Downloaded file does not exist");
} }
const videoFileSize = videoFileInfo.size; const videoFileSize = videoFile.size;
const db = getDownloadsDatabase(); const db = getDownloadsDatabase();
const { item, mediaSource } = process; const { item, mediaSource } = process;
// Only download external subtitles for non-transcoded streams. // Only download external subtitles for non-transcoded streams.
@@ -787,11 +790,12 @@ function useDownloadProvider() {
*/ */
const cleanCacheDirectory = async (): Promise<void> => { const cleanCacheDirectory = async (): Promise<void> => {
try { try {
await FileSystem.deleteAsync(APP_CACHE_DOWNLOAD_DIRECTORY, { if (APP_CACHE_DOWNLOAD_DIRECTORY.exists) {
idempotent: true, APP_CACHE_DOWNLOAD_DIRECTORY.delete();
}); }
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, { APP_CACHE_DOWNLOAD_DIRECTORY.create({
intermediates: true, intermediates: true,
idempotent: true,
}); });
} catch (_error) { } catch (_error) {
toast.error(t("home.downloads.toasts.failed_to_clean_cache_directory")); toast.error(t("home.downloads.toasts.failed_to_clean_cache_directory"));
@@ -898,9 +902,11 @@ function useDownloadProvider() {
} }
if (downloadedItem?.videoFilePath) { if (downloadedItem?.videoFilePath) {
await FileSystem.deleteAsync(downloadedItem.videoFilePath, { try {
idempotent: true, new File(downloadedItem.videoFilePath).delete();
}); } catch (_err) {
// File might not exist, ignore
}
} }
if (downloadedItem?.mediaSource?.MediaStreams) { if (downloadedItem?.mediaSource?.MediaStreams) {
@@ -909,17 +915,21 @@ function useDownloadProvider() {
stream.Type === "Subtitle" && stream.Type === "Subtitle" &&
stream.DeliveryMethod === "External" stream.DeliveryMethod === "External"
) { ) {
await FileSystem.deleteAsync(stream.DeliveryUrl!, { try {
idempotent: true, new File(stream.DeliveryUrl!).delete();
}); } catch (_err) {
// File might not exist, ignore
}
} }
} }
} }
if (downloadedItem?.trickPlayData?.path) { if (downloadedItem?.trickPlayData?.path) {
await FileSystem.deleteAsync(downloadedItem.trickPlayData.path, { try {
idempotent: true, new Directory(downloadedItem.trickPlayData.path).delete();
}); } catch (_err) {
// Directory might not exist, ignore
}
} }
await saveDownloadsDatabase(db); await saveDownloadsDatabase(db);
@@ -989,21 +999,17 @@ function useDownloadProvider() {
* @returns The size of the app and the remaining space on the device. * @returns The size of the app and the remaining space on the device.
*/ */
const appSizeUsage = async () => { const appSizeUsage = async () => {
const [total, remaining] = await Promise.all([ const total = Paths.totalDiskSpace;
FileSystem.getTotalDiskCapacityAsync(), const remaining = Paths.availableDiskSpace;
FileSystem.getFreeDiskStorageAsync(),
]);
let appSize = 0; let appSize = 0;
const downloadedFiles = await FileSystem.readDirectoryAsync( const documentDir = Paths.document;
`${FileSystem.documentDirectory!}`, const contents = documentDir.list();
); for (const item of contents) {
for (const file of downloadedFiles) { if (item instanceof File) {
const fileInfo = await FileSystem.getInfoAsync( appSize += item.size;
`${FileSystem.documentDirectory!}${file}`, } else if (item instanceof Directory) {
); appSize += item.size || 0;
if (fileInfo.exists) {
appSize += fileInfo.size;
} }
} }
return { total, remaining, appSize: appSize }; return { total, remaining, appSize: appSize };
@@ -1208,7 +1214,7 @@ function useDownloadProvider() {
deleteFileByType, deleteFileByType,
getDownloadedItemSize, getDownloadedItemSize,
getDownloadedItemById, getDownloadedItemById,
APP_CACHE_DOWNLOAD_DIRECTORY, APP_CACHE_DOWNLOAD_DIRECTORY: APP_CACHE_DOWNLOAD_DIRECTORY.uri,
cleanCacheDirectory, cleanCacheDirectory,
updateDownloadedItem, updateDownloadedItem,
appSizeUsage, appSizeUsage,

View File

@@ -1523,9 +1523,9 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@kesha-antonov/react-native-background-downloader@github:fredrikburmester/react-native-background-downloader#f3bf69ad124b6ec6adbc30c7f688935d0376fc56": "@kesha-antonov/react-native-background-downloader@github:fredrikburmester/react-native-background-downloader#dce095fd2a5f257ab3e2e49252fbe206ad994202":
version "3.2.6" version "3.2.6"
resolved "https://codeload.github.com/fredrikburmester/react-native-background-downloader/tar.gz/f3bf69ad124b6ec6adbc30c7f688935d0376fc56" resolved "https://codeload.github.com/fredrikburmester/react-native-background-downloader/tar.gz/dce095fd2a5f257ab3e2e49252fbe206ad994202"
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"