New delete options & storage visibility

- Added react-native-progress dependency
- Added bottom sheet to downloads page to handle actions for deleting items by type
- Added ability to long press to delete a single series
- Added ability to delete by season
- Refactored delete helpers in DownloadProvider.tsx
- Display storage usage inside downloads & settings page
- Fixed Delete all downloaded files from delting user data in mmkv
This commit is contained in:
herrrta
2024-12-02 21:42:13 -05:00
parent 69ffdc2ddf
commit b73c29221a
8 changed files with 368 additions and 183 deletions

View File

@@ -47,6 +47,8 @@ import { getItemImage } from "@/utils/getItemImage";
import useImageStorage from "@/hooks/useImageStorage";
import { storage } from "@/utils/mmkv";
import useDownloadHelper from "@/utils/download";
import {FileInfo} from "expo-file-system";
import * as Haptics from "expo-haptics";
export type DownloadedItem = {
item: Partial<BaseItemDto>;
@@ -388,19 +390,20 @@ function useDownloadProvider() {
);
const deleteAllFiles = async (): Promise<void> => {
try {
await deleteLocalFiles();
removeDownloadedItemsFromStorage();
await cancelAllServerJobs();
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
toast.success("All files, folders, and jobs deleted successfully");
} catch (error) {
console.error("Failed to delete all files, folders, and jobs:", error);
Promise.all([
deleteLocalFiles(),
removeDownloadedItemsFromStorage(),
cancelAllServerJobs(),
queryClient.invalidateQueries({queryKey: ["downloadedItems"]}),
]).then(() =>
toast.success("All files, folders, and jobs deleted successfully")
).catch((reason) => {
console.error("Failed to delete all files, folders, and jobs:", reason);
toast.error("An error occurred while deleting files and jobs");
}
});
};
const deleteLocalFiles = async (): Promise<void> => {
const forEveryDirectory = async (callback: (dir: FileInfo) => void) => {
const baseDirectory = FileSystem.documentDirectory;
if (!baseDirectory) {
throw new Error("Base directory not found");
@@ -408,25 +411,36 @@ function useDownloadProvider() {
const dirContents = await FileSystem.readDirectoryAsync(baseDirectory);
for (const item of dirContents) {
const itemPath = `${baseDirectory}${item}`;
const itemInfo = await FileSystem.getInfoAsync(itemPath);
// Exclude mmkv directory.
// Deleting this deletes all user information as well. Logout should handle this.
if (item == "mmkv")
continue
const itemInfo = await FileSystem.getInfoAsync(`${baseDirectory}${item}`);
if (itemInfo.exists) {
if (itemInfo.isDirectory) {
await FileSystem.deleteAsync(itemPath, { idempotent: true });
} else {
await FileSystem.deleteAsync(itemPath, { idempotent: true });
}
callback(itemInfo)
}
}
}
const deleteLocalFiles = async (): Promise<void> => {
await forEveryDirectory((dir) => {
console.warn("Deleting file", dir.uri)
FileSystem.deleteAsync(dir.uri, {idempotent: true})
}
)
};
const removeDownloadedItemsFromStorage = (): void => {
try {
storage.delete("downloadedItems");
} catch (error) {
console.error("Failed to remove downloadedItems from storage:", error);
throw error;
}
const removeDownloadedItemsFromStorage = async () => {
// delete any saved images first
Promise.all([
deleteFileByType("Movie"),
deleteFileByType("Episode"),
]).then(() =>
storage.delete("downloadedItems")
).catch((reason) => {
console.error("Failed to remove downloadedItems from storage:", reason);
throw reason
})
};
const cancelAllServerJobs = async (): Promise<void> => {
@@ -434,7 +448,8 @@ function useDownloadProvider() {
throw new Error("No auth header available");
}
if (!settings?.optimizedVersionsServerUrl) {
throw new Error("No server URL configured");
console.error("No server URL configured");
return
}
const deviceId = await getOrSetDeviceId();
@@ -494,6 +509,41 @@ function useDownloadProvider() {
}
};
const deleteItems = async (items: BaseItemDto[]) => {
Promise.all(items.map(i => {
if (i.Id)
return deleteFile(i.Id)
return
})).then(() =>
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
)
}
const deleteFileByType = async (type: BaseItemDto['Type']) => {
await Promise.all(
downloadedFiles
?.filter(file => file.item.Type == type)
?.flatMap(file => {
const promises = [];
if (type == "Episode" && file.item.SeriesId)
promises.push(deleteFile(file.item.SeriesId))
promises.push(deleteFile(file.item.Id!))
return promises;
})
|| []
);
}
const getAppSizeUsage = async () => {
const sizes: number[] = [];
await forEveryDirectory(dir => {
if (dir.exists)
sizes.push(dir.size)
})
return sizes.reduce((sum, size) => sum + size, 0);
}
function getDownloadedItem(itemId: string): DownloadedItem | null {
try {
const downloadedItems = storage.getString("downloadedItems");
@@ -566,11 +616,14 @@ function useDownloadProvider() {
downloadedFiles,
deleteAllFiles,
deleteFile,
deleteItems,
saveDownloadedItemInfo,
removeProcess,
setProcesses,
startDownload,
getDownloadedItem,
deleteFileByType,
getAppSizeUsage
};
}
@@ -591,3 +644,11 @@ export function useDownload() {
}
return context;
}
export function bytesToReadable(bytes: number): string {
const gb = bytes / 1e+9;
if (gb >= 1)
return `${gb.toFixed(2)} GB`
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
}