mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
wip
This commit is contained in:
@@ -2,16 +2,17 @@ import { Text } from "@/components/common/Text";
|
|||||||
import { MovieCard } from "@/components/downloads/MovieCard";
|
import { MovieCard } from "@/components/downloads/MovieCard";
|
||||||
import { SeriesCard } from "@/components/downloads/SeriesCard";
|
import { SeriesCard } from "@/components/downloads/SeriesCard";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
|
import { getAllDownloadedItems } from "@/hooks/useDownloadM3U8Files";
|
||||||
import { runningProcesses } from "@/utils/atoms/downloads";
|
import { runningProcesses } from "@/utils/atoms/downloads";
|
||||||
import { queueAtom } from "@/utils/atoms/queue";
|
import { queueAtom } from "@/utils/atoms/queue";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { FFmpegKit } from "ffmpeg-kit-react-native";
|
import { FFmpegKit } from "ffmpeg-kit-react-native";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
import { ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
@@ -21,10 +22,7 @@ const downloads: React.FC = () => {
|
|||||||
|
|
||||||
const { data: downloadedFiles, isLoading } = useQuery({
|
const { data: downloadedFiles, isLoading } = useQuery({
|
||||||
queryKey: ["downloaded_files", process?.item.Id],
|
queryKey: ["downloaded_files", process?.item.Id],
|
||||||
queryFn: async () =>
|
queryFn: getAllDownloadedItems,
|
||||||
JSON.parse(
|
|
||||||
(await AsyncStorage.getItem("downloaded_files")) || "[]"
|
|
||||||
) as BaseItemDto[],
|
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,6 +52,59 @@ const downloads: React.FC = () => {
|
|||||||
return formatNumber(timeLeft / 10000);
|
return formatNumber(timeLeft / 10000);
|
||||||
}, [process]);
|
}, [process]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const dir = FileSystem.documentDirectory;
|
||||||
|
if (dir) {
|
||||||
|
const items = await FileSystem.readDirectoryAsync(dir);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
console.log("No items found in the document directory.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = `${dir}${item}`;
|
||||||
|
const info = await FileSystem.getInfoAsync(fullPath);
|
||||||
|
|
||||||
|
if (info.exists) {
|
||||||
|
if (info.isDirectory) {
|
||||||
|
// List items in the directory
|
||||||
|
const subItems = await FileSystem.readDirectoryAsync(fullPath);
|
||||||
|
if (subItems.length === 0) {
|
||||||
|
console.log(`Directory ${item} is empty.`);
|
||||||
|
} else {
|
||||||
|
console.log(`Items in ${item}:`, subItems);
|
||||||
|
// If item ends in m3u8, print the content of the file
|
||||||
|
const m3u8Files = subItems.filter((subItem) =>
|
||||||
|
subItem.endsWith(".m3u8")
|
||||||
|
);
|
||||||
|
if (m3u8Files.length === 0) {
|
||||||
|
console.log(`No .m3u8 files found in ${item}.`);
|
||||||
|
} else {
|
||||||
|
for (let subItem of m3u8Files) {
|
||||||
|
console.log(
|
||||||
|
`Content of ${subItem}:`,
|
||||||
|
await FileSystem.readAsStringAsync(
|
||||||
|
`${fullPath}/${subItem}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`${item} is a file`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`${item} does not exist.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Document directory is not available.");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -108,10 +108,18 @@ export default function settings() {
|
|||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
|
try {
|
||||||
await deleteAllFiles();
|
await deleteAllFiles();
|
||||||
Haptics.notificationAsync(
|
Haptics.notificationAsync(
|
||||||
Haptics.NotificationFeedbackType.Success
|
Haptics.NotificationFeedbackType.Success
|
||||||
);
|
);
|
||||||
|
toast.success("All files deleted");
|
||||||
|
} catch (e) {
|
||||||
|
Haptics.notificationAsync(
|
||||||
|
Haptics.NotificationFeedbackType.Error
|
||||||
|
);
|
||||||
|
toast.error("Error deleting files");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete all downloaded files
|
Delete all downloaded files
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { Loader } from "./Loader";
|
|||||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
import ProgressCircle from "./ProgressCircle";
|
import ProgressCircle from "./ProgressCircle";
|
||||||
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
||||||
|
import { useDownloadM3U8Files } from "@/hooks/useDownloadM3U8Files";
|
||||||
|
|
||||||
interface DownloadProps extends ViewProps {
|
interface DownloadProps extends ViewProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -42,7 +43,10 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item, ...props }) => {
|
|||||||
const [process] = useAtom(runningProcesses);
|
const [process] = useAtom(runningProcesses);
|
||||||
const [queue, setQueue] = useAtom(queueAtom);
|
const [queue, setQueue] = useAtom(queueAtom);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const { startRemuxing } = useRemuxHlsToMp4(item);
|
// const { startRemuxing } = useRemuxHlsToMp4(item);
|
||||||
|
|
||||||
|
const { cancelDownload, startBackgroundDownload } =
|
||||||
|
useDownloadM3U8Files(item);
|
||||||
|
|
||||||
const [selectedMediaSource, setSelectedMediaSource] =
|
const [selectedMediaSource, setSelectedMediaSource] =
|
||||||
useState<MediaSourceInfo | null>(null);
|
useState<MediaSourceInfo | null>(null);
|
||||||
@@ -153,11 +157,11 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item, ...props }) => {
|
|||||||
|
|
||||||
if (!url) throw new Error("No url");
|
if (!url) throw new Error("No url");
|
||||||
|
|
||||||
return await startRemuxing(url);
|
return await startBackgroundDownload(url);
|
||||||
}, [
|
}, [
|
||||||
api,
|
api,
|
||||||
item,
|
item,
|
||||||
startRemuxing,
|
startBackgroundDownload,
|
||||||
user?.Id,
|
user?.Id,
|
||||||
selectedMediaSource,
|
selectedMediaSource,
|
||||||
selectedAudioStream,
|
selectedAudioStream,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Text } from "../common/Text";
|
|||||||
|
|
||||||
import { usePlayback } from "@/providers/PlaybackProvider";
|
import { usePlayback } from "@/providers/PlaybackProvider";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
import { deleteDownloadedItem } from "@/hooks/useDownloadM3U8Files";
|
||||||
|
|
||||||
interface MovieCardProps {
|
interface MovieCardProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -26,13 +27,32 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { startDownloadedFilePlayback } = usePlayback();
|
const { startDownloadedFilePlayback } = usePlayback();
|
||||||
|
|
||||||
const handleOpenFile = useCallback(() => {
|
const handleOpenFile = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const directoryPath = `${FileSystem.documentDirectory}${item.Id}`;
|
||||||
|
const m3u8FilePath = `${directoryPath}/local.m3u8`;
|
||||||
|
|
||||||
|
console.log("Path: ", m3u8FilePath);
|
||||||
|
|
||||||
|
// Check if the m3u8 file exists
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(m3u8FilePath);
|
||||||
|
|
||||||
|
if (!fileInfo.exists) {
|
||||||
|
console.warn("m3u8 file does not exist:", m3u8FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start playback
|
||||||
startDownloadedFilePlayback({
|
startDownloadedFilePlayback({
|
||||||
item,
|
item,
|
||||||
url: `${FileSystem.documentDirectory}/${item.Id}.mp4`,
|
url: `${m3u8FilePath}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Navigate to the play screen
|
||||||
router.push("/play");
|
router.push("/play");
|
||||||
}, [item, startDownloadedFilePlayback]);
|
} catch (error) {
|
||||||
|
console.error("Error opening file:", error);
|
||||||
|
}
|
||||||
|
}, [item, startDownloadedFilePlayback, router, deleteDownloadedItem]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deleting the file with haptic feedback.
|
* Handles deleting the file with haptic feedback.
|
||||||
|
|||||||
195
hooks/useDownloadM3U8Files.ts
Normal file
195
hooks/useDownloadM3U8Files.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { runningProcesses } from "@/utils/atoms/downloads";
|
||||||
|
import { writeToLog } from "@/utils/log";
|
||||||
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { download } from "@kesha-antonov/react-native-background-downloader";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
|
||||||
|
export const useDownloadM3U8Files = (item: BaseItemDto) => {
|
||||||
|
const [_, setProgress] = useAtom(runningProcesses);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [api] = useAtom(apiAtom);
|
||||||
|
|
||||||
|
if (!item.Id || !item.Name) {
|
||||||
|
throw new Error("Item must have an Id and Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const startBackgroundDownload = useCallback(
|
||||||
|
async (url: string) => {
|
||||||
|
if (!api) {
|
||||||
|
throw new Error("API is not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Download started", { invert: true });
|
||||||
|
writeToLog("INFO", `Starting download for item ${item.Name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const directoryPath = `${FileSystem.documentDirectory}${item.Id}`;
|
||||||
|
await FileSystem.makeDirectoryAsync(directoryPath, {
|
||||||
|
intermediates: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const m3u8Content = await FileSystem.downloadAsync(
|
||||||
|
url,
|
||||||
|
`${directoryPath}/original.m3u8`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (m3u8Content.status !== 200) {
|
||||||
|
throw new Error("Failed to download m3u8 file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const m3u8Text = await FileSystem.readAsStringAsync(m3u8Content.uri);
|
||||||
|
const segments = await fetchSegmentInfo(
|
||||||
|
m3u8Text,
|
||||||
|
api.basePath,
|
||||||
|
item.Id!
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
const segment = segments[i];
|
||||||
|
const segmentUrl = `${api.basePath}/videos/${item.Id}/${segment.path}`;
|
||||||
|
const destination = `${directoryPath}/${i}.ts`;
|
||||||
|
|
||||||
|
await download({
|
||||||
|
id: `${item.Id}_segment_${i}`,
|
||||||
|
url: segmentUrl,
|
||||||
|
destination: destination,
|
||||||
|
}).done((e) => {
|
||||||
|
console.log("Download completed for segment", i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createLocalM3U8File(segments, directoryPath);
|
||||||
|
await saveDownloadedItemInfo(item);
|
||||||
|
|
||||||
|
writeToLog("INFO", `Download completed for item: ${item.Name}`);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["downloaded_files"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["downloaded"] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to download:", error);
|
||||||
|
writeToLog("ERROR", `Download failed for item: ${item.Name}`);
|
||||||
|
setProgress(null);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, setProgress, queryClient, api]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { startBackgroundDownload };
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Segment {
|
||||||
|
duration: number;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSegmentInfo(
|
||||||
|
masterM3U8Content: string,
|
||||||
|
baseUrl: string,
|
||||||
|
itemId: string
|
||||||
|
): Promise<Segment[]> {
|
||||||
|
const lines = masterM3U8Content.split("\n");
|
||||||
|
const mainPlaylistLine = lines.find((line) => line.startsWith("main.m3u8"));
|
||||||
|
|
||||||
|
if (!mainPlaylistLine) {
|
||||||
|
throw new Error("Main playlist URL not found in the master M3U8");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${baseUrl}/videos/${itemId}/${mainPlaylistLine}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const mainPlaylistContent = await response.text();
|
||||||
|
|
||||||
|
const segments: Segment[] = [];
|
||||||
|
const mainPlaylistLines = mainPlaylistContent.split("\n");
|
||||||
|
|
||||||
|
for (let i = 0; i < mainPlaylistLines.length; i++) {
|
||||||
|
if (mainPlaylistLines[i].startsWith("#EXTINF:")) {
|
||||||
|
const durationMatch = mainPlaylistLines[i].match(
|
||||||
|
/#EXTINF:(\d+(?:\.\d+)?)/
|
||||||
|
);
|
||||||
|
const duration = durationMatch ? parseFloat(durationMatch[1]) : 0;
|
||||||
|
const path = mainPlaylistLines[i + 1];
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
segments.push({ duration, path });
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createLocalM3U8File(segments: Segment[], directoryPath: string) {
|
||||||
|
let localM3U8Content = "#EXTM3U\n#EXT-X-VERSION:3\n";
|
||||||
|
localM3U8Content += `#EXT-X-TARGETDURATION:${Math.ceil(
|
||||||
|
Math.max(...segments.map((s) => s.duration))
|
||||||
|
)}\n`;
|
||||||
|
localM3U8Content += "#EXT-X-MEDIA-SEQUENCE:0\n";
|
||||||
|
|
||||||
|
segments.forEach((segment, index) => {
|
||||||
|
localM3U8Content += `#EXTINF:${segment.duration.toFixed(3)},\n`;
|
||||||
|
localM3U8Content += `${directoryPath}/${index}.ts\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
localM3U8Content += "#EXT-X-ENDLIST\n";
|
||||||
|
|
||||||
|
const localM3U8Path = `${directoryPath}/local.m3u8`;
|
||||||
|
await FileSystem.writeAsStringAsync(localM3U8Path, localM3U8Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDownloadedItemInfo(item: BaseItemDto) {
|
||||||
|
try {
|
||||||
|
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
|
||||||
|
let 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save downloaded item information:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDownloadedItem(itemId: string) {
|
||||||
|
try {
|
||||||
|
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
|
||||||
|
let items: BaseItemDto[] = downloadedItems
|
||||||
|
? JSON.parse(downloadedItems)
|
||||||
|
: [];
|
||||||
|
items = items.filter((item) => item.Id !== itemId);
|
||||||
|
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
|
||||||
|
|
||||||
|
const directoryPath = `${FileSystem.documentDirectory}${itemId}`;
|
||||||
|
await FileSystem.deleteAsync(directoryPath, { idempotent: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete downloaded item:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllDownloadedItems(): Promise<BaseItemDto[]> {
|
||||||
|
try {
|
||||||
|
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
|
||||||
|
if (downloadedItems) {
|
||||||
|
return JSON.parse(downloadedItems) as BaseItemDto[];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to retrieve downloaded items:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,24 +14,28 @@ export const useFiles = () => {
|
|||||||
* Deletes all downloaded files and clears the download record.
|
* Deletes all downloaded files and clears the download record.
|
||||||
*/
|
*/
|
||||||
const deleteAllFiles = async (): Promise<void> => {
|
const deleteAllFiles = async (): Promise<void> => {
|
||||||
const directoryUri = FileSystem.documentDirectory;
|
try {
|
||||||
if (!directoryUri) {
|
// Get all downloaded items
|
||||||
console.error("Document directory is undefined");
|
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
|
||||||
return;
|
if (downloadedItems) {
|
||||||
|
const items = JSON.parse(downloadedItems);
|
||||||
|
|
||||||
|
// Delete each item's folder
|
||||||
|
for (const item of items) {
|
||||||
|
const folderPath = `${FileSystem.documentDirectory}${item.Id}`;
|
||||||
|
await FileSystem.deleteAsync(folderPath, { idempotent: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Clear the downloadedItems in AsyncStorage
|
||||||
const fileNames = await FileSystem.readDirectoryAsync(directoryUri);
|
await AsyncStorage.removeItem("downloadedItems");
|
||||||
await Promise.all(
|
|
||||||
fileNames.map((item) =>
|
// Invalidate the query to refresh the UI
|
||||||
FileSystem.deleteAsync(`${directoryUri}/${item}`, {
|
|
||||||
idempotent: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await AsyncStorage.removeItem("downloaded_files");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["downloaded_files"] });
|
queryClient.invalidateQueries({ queryKey: ["downloaded_files"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["downloaded"] });
|
|
||||||
|
console.log(
|
||||||
|
"Successfully deleted all downloaded files and cleared AsyncStorage"
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete all files:", error);
|
console.error("Failed to delete all files:", error);
|
||||||
}
|
}
|
||||||
@@ -48,22 +52,29 @@ export const useFiles = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FileSystem.deleteAsync(
|
// Delete the entire folder
|
||||||
`${FileSystem.documentDirectory}/${id}.mp4`,
|
const folderPath = `${FileSystem.documentDirectory}${id}`;
|
||||||
{ idempotent: true }
|
await FileSystem.deleteAsync(folderPath, { idempotent: true });
|
||||||
);
|
|
||||||
|
|
||||||
const currentFiles = await getDownloadedFiles();
|
// Remove the item from AsyncStorage
|
||||||
const updatedFiles = currentFiles.filter((f) => f.Id !== id);
|
const downloadedItems = await AsyncStorage.getItem("downloadedItems");
|
||||||
|
if (downloadedItems) {
|
||||||
await AsyncStorage.setItem(
|
let items = JSON.parse(downloadedItems);
|
||||||
"downloaded_files",
|
items = items.filter((item: any) => item.Id !== id);
|
||||||
JSON.stringify(updatedFiles)
|
await AsyncStorage.setItem("downloadedItems", JSON.stringify(items));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
// Invalidate the query to refresh the UI
|
||||||
queryClient.invalidateQueries({ queryKey: ["downloaded_files"] });
|
queryClient.invalidateQueries({ queryKey: ["downloaded_files"] });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Successfully deleted folder and AsyncStorage entry for ID ${id}`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to delete file with ID ${id}:`, error);
|
console.error(
|
||||||
|
`Failed to delete folder and AsyncStorage entry for ID ${id}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@expo/vector-icons": "^14.0.3",
|
"@expo/vector-icons": "^14.0.3",
|
||||||
"@gorhom/bottom-sheet": "^4",
|
"@gorhom/bottom-sheet": "^4",
|
||||||
"@jellyfin/sdk": "^0.10.0",
|
"@jellyfin/sdk": "^0.10.0",
|
||||||
|
"@kesha-antonov/react-native-background-downloader": "^3.2.1",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/netinfo": "11.3.1",
|
"@react-native-community/netinfo": "11.3.1",
|
||||||
"@react-native-menu/menu": "^1.1.3",
|
"@react-native-menu/menu": "^1.1.3",
|
||||||
|
|||||||
48
plugins/withRNBackgroundDownloader.js
Normal file
48
plugins/withRNBackgroundDownloader.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const { withAppDelegate } = require("@expo/config-plugins");
|
||||||
|
|
||||||
|
function withRNBackgroundDownloader(expoConfig) {
|
||||||
|
return withAppDelegate(expoConfig, async (appDelegateConfig) => {
|
||||||
|
const { modResults: appDelegate } = appDelegateConfig;
|
||||||
|
const appDelegateLines = appDelegate.contents.split("\n");
|
||||||
|
|
||||||
|
// Define the code to be added to AppDelegate.mm
|
||||||
|
const backgroundDownloaderImport =
|
||||||
|
"#import <RNBackgroundDownloader.h> // Required by react-native-background-downloader. Generated by expoPlugins/withRNBackgroundDownloader.js";
|
||||||
|
const backgroundDownloaderDelegate = `\n// Delegate method required by react-native-background-downloader. Generated by expoPlugins/withRNBackgroundDownloader.js
|
||||||
|
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
|
||||||
|
{
|
||||||
|
[RNBackgroundDownloader setCompletionHandlerWithIdentifier:identifier completionHandler:completionHandler];
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// Find the index of the AppDelegate import statement
|
||||||
|
const importIndex = appDelegateLines.findIndex((line) =>
|
||||||
|
/^#import "AppDelegate.h"/.test(line)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the index of the last line before the @end statement
|
||||||
|
const endStatementIndex = appDelegateLines.findIndex((line) =>
|
||||||
|
/@end/.test(line)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert the import statement if it's not already present
|
||||||
|
if (!appDelegate.contents.includes(backgroundDownloaderImport)) {
|
||||||
|
appDelegateLines.splice(importIndex + 1, 0, backgroundDownloaderImport);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the delegate method above the @end statement
|
||||||
|
if (!appDelegate.contents.includes(backgroundDownloaderDelegate)) {
|
||||||
|
appDelegateLines.splice(
|
||||||
|
endStatementIndex,
|
||||||
|
0,
|
||||||
|
backgroundDownloaderDelegate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the contents of the AppDelegate file
|
||||||
|
appDelegate.contents = appDelegateLines.join("\n");
|
||||||
|
|
||||||
|
return appDelegateConfig;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withRNBackgroundDownloader;
|
||||||
Reference in New Issue
Block a user