mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-16 08:08:18 +00:00
fix: rn downloads
This commit is contained in:
@@ -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
|
||||
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
|
||||
|
||||
### Modal doesn't appear
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { useNavigation } from "expo-router";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
||||
|
||||
@@ -28,13 +28,9 @@ import {
|
||||
} from "@/utils/log";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
|
||||
// TEMPORARILY DISABLED
|
||||
// To re-enable: Move package from "disabledDependencies" to "dependencies" in package.json,
|
||||
// run "bun install", then uncomment the require below and remove the null assignment
|
||||
// const BackGroundDownloader = !Platform.isTV
|
||||
// ? require("@kesha-antonov/react-native-background-downloader")
|
||||
// : null;
|
||||
const BackGroundDownloader = null;
|
||||
const BackGroundDownloader = !Platform.isTV
|
||||
? require("@kesha-antonov/react-native-background-downloader")
|
||||
: null;
|
||||
|
||||
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
|
||||
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 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;
|
||||
|
||||
@@ -149,7 +145,7 @@ if (!Platform.isTV) {
|
||||
|
||||
const token = getTokenFromStorage();
|
||||
const deviceId = getOrSetDeviceId();
|
||||
const baseDirectory = FileSystem.documentDirectory;
|
||||
const baseDirectory = Paths.document.uri;
|
||||
|
||||
if (!token || !deviceId || !baseDirectory)
|
||||
return BackgroundTask.BackgroundTaskResult.Failed;
|
||||
@@ -426,10 +422,10 @@ function Layout() {
|
||||
<LogProvider>
|
||||
<WebSocketProvider>
|
||||
<DownloadProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<GlobalModalProvider>
|
||||
<SystemBars style='light' hidden={false} />
|
||||
<GlobalModalProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<SystemBars style='light' hidden={false} />
|
||||
<Stack initialRouteName='(auth)/(tabs)'>
|
||||
<Stack.Screen
|
||||
name='(auth)/(tabs)'
|
||||
@@ -471,10 +467,10 @@ function Layout() {
|
||||
}}
|
||||
closeButton
|
||||
/>
|
||||
<GlobalModal />
|
||||
</ThemeProvider>
|
||||
<GlobalModal />
|
||||
</GlobalModalProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</GlobalModalProvider>
|
||||
</DownloadProvider>
|
||||
</WebSocketProvider>
|
||||
</LogProvider>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { t } from "i18next";
|
||||
import { useAtomValue } from "jotai";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
@@ -82,10 +81,10 @@ const Login: React.FC = () => {
|
||||
onPress={() => {
|
||||
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} />
|
||||
<Text className='ml-2 text-purple-600'>
|
||||
<Text className=' ml-1 text-purple-600'>
|
||||
{t("login.change_server")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -34,7 +34,6 @@ export const AudioTrackSelector: React.FC<Props> = ({
|
||||
const optionGroups: OptionGroup[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "Audio streams",
|
||||
options:
|
||||
audioStreams?.map((audio, idx) => ({
|
||||
type: "radio" as const,
|
||||
@@ -71,26 +70,19 @@ export const AudioTrackSelector: React.FC<Props> = ({
|
||||
if (isTv) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
className='flex shrink'
|
||||
style={{
|
||||
minWidth: 50,
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.audio")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
>
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.audio")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,18 +79,16 @@ export const BitrateSelector: React.FC<Props> = ({
|
||||
const optionGroups: OptionGroup[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "bitrates",
|
||||
title: "Bitrates",
|
||||
options: sorted.map((bitrate) => ({
|
||||
id: bitrate.key,
|
||||
type: "radio" as const,
|
||||
groupId: "bitrates",
|
||||
label: bitrate.key,
|
||||
value: bitrate,
|
||||
selected: bitrate.value === selected?.value,
|
||||
onPress: () => onChange(bitrate),
|
||||
})),
|
||||
},
|
||||
],
|
||||
[sorted, selected],
|
||||
[sorted, selected, onChange],
|
||||
);
|
||||
|
||||
const handleOptionSelect = (optionId: string) => {
|
||||
@@ -118,27 +116,19 @@ export const BitrateSelector: React.FC<Props> = ({
|
||||
if (isTv) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
className='flex shrink'
|
||||
style={{
|
||||
minWidth: 60,
|
||||
maxWidth: 200,
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.quality")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
>
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.quality")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -359,16 +359,18 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='flex flex-col space-y-2 w-full items-start'>
|
||||
<BitrateSelector
|
||||
inverted
|
||||
onChange={(val) =>
|
||||
setSelectedOptions(
|
||||
(prev) => prev && { ...prev, bitrate: val },
|
||||
)
|
||||
}
|
||||
selected={selectedOptions?.bitrate}
|
||||
/>
|
||||
<View className='flex flex-col space-y-2 w-full'>
|
||||
<View className='items-start'>
|
||||
<BitrateSelector
|
||||
inverted
|
||||
onChange={(val) =>
|
||||
setSelectedOptions(
|
||||
(prev) => prev && { ...prev, bitrate: val },
|
||||
)
|
||||
}
|
||||
selected={selectedOptions?.bitrate}
|
||||
/>
|
||||
</View>
|
||||
{itemsNotDownloaded.length > 1 && (
|
||||
<View className='flex flex-row items-center justify-between w-full py-2'>
|
||||
<Text>{t("item_card.download.download_unwatched_only")}</Text>
|
||||
@@ -380,21 +382,23 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
)}
|
||||
{itemsNotDownloaded.length === 1 && (
|
||||
<View>
|
||||
<MediaSourceSelector
|
||||
item={items[0]}
|
||||
onChange={(val) =>
|
||||
setSelectedOptions(
|
||||
(prev) =>
|
||||
prev && {
|
||||
...prev,
|
||||
mediaSource: val,
|
||||
},
|
||||
)
|
||||
}
|
||||
selected={selectedOptions?.mediaSource}
|
||||
/>
|
||||
<View className='items-start'>
|
||||
<MediaSourceSelector
|
||||
item={items[0]}
|
||||
onChange={(val) =>
|
||||
setSelectedOptions(
|
||||
(prev) =>
|
||||
prev && {
|
||||
...prev,
|
||||
mediaSource: val,
|
||||
},
|
||||
)
|
||||
}
|
||||
selected={selectedOptions?.mediaSource}
|
||||
/>
|
||||
</View>
|
||||
{selectedOptions?.mediaSource && (
|
||||
<View className='flex flex-col space-y-2'>
|
||||
<View className='flex flex-col space-y-2 items-start'>
|
||||
<AudioTrackSelector
|
||||
source={selectedOptions.mediaSource}
|
||||
onChange={(val) => {
|
||||
@@ -427,11 +431,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
className='mt-auto'
|
||||
onPress={acceptDownloadOptions}
|
||||
color='purple'
|
||||
>
|
||||
<Button onPress={acceptDownloadOptions} color='purple'>
|
||||
{t("item_card.download.download_button")}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -47,19 +47,17 @@ export const MediaSourceSelector: React.FC<Props> = ({
|
||||
const optionGroups: OptionGroup[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "media-sources",
|
||||
title: "Media sources",
|
||||
options:
|
||||
item.MediaSources?.map((source, idx) => ({
|
||||
id: `${source.Id || idx}`,
|
||||
item.MediaSources?.map((source) => ({
|
||||
type: "radio" as const,
|
||||
groupId: "media-sources",
|
||||
label: getDisplayName(source),
|
||||
value: source,
|
||||
selected: source.Id === selected?.Id,
|
||||
onPress: () => onChange(source),
|
||||
})) || [],
|
||||
},
|
||||
],
|
||||
[item.MediaSources, selected, getDisplayName],
|
||||
[item.MediaSources, selected, getDisplayName, onChange],
|
||||
);
|
||||
|
||||
const handleOptionSelect = (optionId: string) => {
|
||||
@@ -87,26 +85,19 @@ export const MediaSourceSelector: React.FC<Props> = ({
|
||||
if (isTv) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
className='flex shrink'
|
||||
style={{
|
||||
minWidth: 50,
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.video")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
>
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.video")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,29 +33,27 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
|
||||
const optionGroups: OptionGroup[] = useMemo(() => {
|
||||
const options = [
|
||||
{
|
||||
id: "none",
|
||||
type: "radio" as const,
|
||||
groupId: "subtitle-streams",
|
||||
label: t("item_card.none"),
|
||||
value: -1,
|
||||
selected: selected === -1,
|
||||
onPress: () => onChange(-1),
|
||||
},
|
||||
...(subtitleStreams?.map((subtitle, idx) => ({
|
||||
id: `${subtitle.Index || idx}`,
|
||||
type: "radio" as const,
|
||||
groupId: "subtitle-streams",
|
||||
label: subtitle.DisplayTitle || `Subtitle Stream ${idx + 1}`,
|
||||
value: subtitle.Index,
|
||||
selected: subtitle.Index === selected,
|
||||
onPress: () => onChange(subtitle.Index ?? -1),
|
||||
})) || []),
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
id: "subtitle-streams",
|
||||
title: "Subtitle tracks",
|
||||
options,
|
||||
},
|
||||
];
|
||||
}, [subtitleStreams, selected, t]);
|
||||
}, [subtitleStreams, selected, t, onChange]);
|
||||
|
||||
const handleOptionSelect = (optionId: string) => {
|
||||
if (optionId === "none") {
|
||||
@@ -96,27 +94,19 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
|
||||
if (Platform.isTV || subtitleStreams?.length === 0) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
className='flex col shrink justify-start place-self-start items-start'
|
||||
style={{
|
||||
minWidth: 60,
|
||||
maxWidth: 200,
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.subtitles")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
>
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
trigger={trigger}
|
||||
title={t("item_card.subtitles")}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onOptionSelect={handleOptionSelect}
|
||||
expoUIConfig={{
|
||||
hostStyle: { flex: 1 },
|
||||
}}
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
bottomSheetConfig={{
|
||||
enablePanDownToClose: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
/** @type {import('expo/metro-config').MetroConfig} */
|
||||
const config = getDefaultConfig(__dirname); // eslint-disable-line no-undef
|
||||
@@ -23,6 +25,27 @@ if (process.env?.EXPO_TV === "1") {
|
||||
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;
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -3,12 +3,12 @@ import type {
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
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 { router } from "expo-router";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { throttle } from "lodash";
|
||||
import React, {
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -341,7 +341,10 @@ function useDownloadProvider() {
|
||||
return api?.accessToken;
|
||||
}, [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 file = storage.getString(DOWNLOADS_DATABASE_KEY);
|
||||
@@ -406,20 +409,17 @@ function useDownloadProvider() {
|
||||
}
|
||||
|
||||
const filename = generateFilename(item);
|
||||
const trickplayDir = `${FileSystem.documentDirectory}${filename}_trickplay/`;
|
||||
await FileSystem.makeDirectoryAsync(trickplayDir, { intermediates: true });
|
||||
const trickplayDir = new Directory(Paths.document, `${filename}_trickplay`);
|
||||
trickplayDir.create({ intermediates: true });
|
||||
let totalSize = 0;
|
||||
|
||||
for (let index = 0; index < trickplayInfo.totalImageSheets; index++) {
|
||||
const url = generateTrickplayUrl(item, index);
|
||||
if (!url) continue;
|
||||
const destination = `${trickplayDir}${index}.jpg`;
|
||||
const destination = new File(trickplayDir, `${index}.jpg`);
|
||||
try {
|
||||
await FileSystem.downloadAsync(url, destination);
|
||||
const fileInfo = await FileSystem.getInfoAsync(destination);
|
||||
if (fileInfo.exists) {
|
||||
totalSize += fileInfo.size;
|
||||
}
|
||||
await File.downloadFileAsync(url, destination);
|
||||
totalSize += destination.size;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`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) => {
|
||||
const url = api.basePath + subtitle.DeliveryUrl;
|
||||
const filename = generateFilename(item);
|
||||
const destination = `${FileSystem.documentDirectory}${filename}_subtitle_${subtitle.Index}`;
|
||||
await FileSystem.downloadAsync(url, destination);
|
||||
subtitle.DeliveryUrl = destination;
|
||||
const destination = new File(
|
||||
Paths.document,
|
||||
`${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 videoFilePath = `${FileSystem.documentDirectory}${filename}.mp4`;
|
||||
const videoFilePath = new File(Paths.document, `${filename}.mp4`).uri;
|
||||
BackGroundDownloader?.download({
|
||||
id: process.id,
|
||||
url: process.inputUrl,
|
||||
@@ -596,11 +599,11 @@ function useDownloadProvider() {
|
||||
)
|
||||
.done(async () => {
|
||||
const trickPlayData = await downloadTrickplayImages(process.item);
|
||||
const videoFileInfo = await FileSystem.getInfoAsync(videoFilePath);
|
||||
if (!videoFileInfo.exists) {
|
||||
const videoFile = new File(videoFilePath);
|
||||
if (!videoFile.exists) {
|
||||
throw new Error("Downloaded file does not exist");
|
||||
}
|
||||
const videoFileSize = videoFileInfo.size;
|
||||
const videoFileSize = videoFile.size;
|
||||
const db = getDownloadsDatabase();
|
||||
const { item, mediaSource } = process;
|
||||
// Only download external subtitles for non-transcoded streams.
|
||||
@@ -787,11 +790,12 @@ function useDownloadProvider() {
|
||||
*/
|
||||
const cleanCacheDirectory = async (): Promise<void> => {
|
||||
try {
|
||||
await FileSystem.deleteAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
|
||||
idempotent: true,
|
||||
});
|
||||
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
|
||||
if (APP_CACHE_DOWNLOAD_DIRECTORY.exists) {
|
||||
APP_CACHE_DOWNLOAD_DIRECTORY.delete();
|
||||
}
|
||||
APP_CACHE_DOWNLOAD_DIRECTORY.create({
|
||||
intermediates: true,
|
||||
idempotent: true,
|
||||
});
|
||||
} catch (_error) {
|
||||
toast.error(t("home.downloads.toasts.failed_to_clean_cache_directory"));
|
||||
@@ -898,9 +902,11 @@ function useDownloadProvider() {
|
||||
}
|
||||
|
||||
if (downloadedItem?.videoFilePath) {
|
||||
await FileSystem.deleteAsync(downloadedItem.videoFilePath, {
|
||||
idempotent: true,
|
||||
});
|
||||
try {
|
||||
new File(downloadedItem.videoFilePath).delete();
|
||||
} catch (_err) {
|
||||
// File might not exist, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedItem?.mediaSource?.MediaStreams) {
|
||||
@@ -909,17 +915,21 @@ function useDownloadProvider() {
|
||||
stream.Type === "Subtitle" &&
|
||||
stream.DeliveryMethod === "External"
|
||||
) {
|
||||
await FileSystem.deleteAsync(stream.DeliveryUrl!, {
|
||||
idempotent: true,
|
||||
});
|
||||
try {
|
||||
new File(stream.DeliveryUrl!).delete();
|
||||
} catch (_err) {
|
||||
// File might not exist, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedItem?.trickPlayData?.path) {
|
||||
await FileSystem.deleteAsync(downloadedItem.trickPlayData.path, {
|
||||
idempotent: true,
|
||||
});
|
||||
try {
|
||||
new Directory(downloadedItem.trickPlayData.path).delete();
|
||||
} catch (_err) {
|
||||
// Directory might not exist, ignore
|
||||
}
|
||||
}
|
||||
|
||||
await saveDownloadsDatabase(db);
|
||||
@@ -989,21 +999,17 @@ function useDownloadProvider() {
|
||||
* @returns The size of the app and the remaining space on the device.
|
||||
*/
|
||||
const appSizeUsage = async () => {
|
||||
const [total, remaining] = await Promise.all([
|
||||
FileSystem.getTotalDiskCapacityAsync(),
|
||||
FileSystem.getFreeDiskStorageAsync(),
|
||||
]);
|
||||
const total = Paths.totalDiskSpace;
|
||||
const remaining = Paths.availableDiskSpace;
|
||||
|
||||
let appSize = 0;
|
||||
const downloadedFiles = await FileSystem.readDirectoryAsync(
|
||||
`${FileSystem.documentDirectory!}`,
|
||||
);
|
||||
for (const file of downloadedFiles) {
|
||||
const fileInfo = await FileSystem.getInfoAsync(
|
||||
`${FileSystem.documentDirectory!}${file}`,
|
||||
);
|
||||
if (fileInfo.exists) {
|
||||
appSize += fileInfo.size;
|
||||
const documentDir = Paths.document;
|
||||
const contents = documentDir.list();
|
||||
for (const item of contents) {
|
||||
if (item instanceof File) {
|
||||
appSize += item.size;
|
||||
} else if (item instanceof Directory) {
|
||||
appSize += item.size || 0;
|
||||
}
|
||||
}
|
||||
return { total, remaining, appSize: appSize };
|
||||
@@ -1208,7 +1214,7 @@ function useDownloadProvider() {
|
||||
deleteFileByType,
|
||||
getDownloadedItemSize,
|
||||
getDownloadedItemById,
|
||||
APP_CACHE_DOWNLOAD_DIRECTORY,
|
||||
APP_CACHE_DOWNLOAD_DIRECTORY: APP_CACHE_DOWNLOAD_DIRECTORY.uri,
|
||||
cleanCacheDirectory,
|
||||
updateDownloadedItem,
|
||||
appSizeUsage,
|
||||
|
||||
@@ -1523,9 +1523,9 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@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"
|
||||
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":
|
||||
version "2.1.5"
|
||||
|
||||
Reference in New Issue
Block a user