This commit is contained in:
Fredrik Burmester
2024-08-06 21:16:37 +02:00
parent 165a9ddde7
commit 57e33428dc
29 changed files with 1113 additions and 296 deletions

View File

@@ -6,11 +6,11 @@ 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 { useEffect, useMemo } from "react";
import { ScrollView, View } from "react-native";
import { ActivityIndicator, ScrollView, View } from "react-native";
import * as FileSystem from "expo-file-system";
const downloads: React.FC = () => {
const { data: downloadedFiles } = useQuery({
const { data: downloadedFiles, isLoading } = useQuery({
queryKey: ["downloaded_files"],
queryFn: async () =>
JSON.parse(
@@ -19,7 +19,7 @@ const downloads: React.FC = () => {
});
const movies = useMemo(
() => downloadedFiles?.filter((f) => f.Type === "Movie"),
() => downloadedFiles?.filter((f) => f.Type === "Movie") || [],
[downloadedFiles]
);
@@ -43,40 +43,45 @@ const downloads: React.FC = () => {
);
}, [downloadedFiles]);
useEffect(() => {
// Get all files from FileStorage
// const filename = `${itemId}.mp4`;
// const fileUri = `${FileSystem.documentDirectory}`;
(async () => {
if (!FileSystem.documentDirectory) return;
const f = await FileSystem.readDirectoryAsync(
FileSystem.documentDirectory
);
console.log("files", FileSystem.documentDirectory, f);
})();
}, []);
if (isLoading) {
return (
<View className="h-full flex flex-col items-center justify-center -mt-6">
<ActivityIndicator size="small" color="white" />
</View>
);
}
if (downloadedFiles?.length === 0) {
return (
<View className="h-full flex flex-col items-center justify-center -mt-6">
<Text className="text-white text-lg font-bold">
No downloaded files
</Text>
</View>
);
}
return (
<ScrollView>
<View className="px-4 py-4">
<View className="mb-4">
<View className="flex flex-row items-center justify-between mb-2">
<Text className="text-2xl font-bold">Movies</Text>
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
<Text className="text-xs font-bold">{movies?.length}</Text>
{movies.length > 0 && (
<View className="mb-4">
<View className="flex flex-row items-center justify-between mb-2">
<Text className="text-2xl font-bold">Movies</Text>
<View className="bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center">
<Text className="text-xs font-bold">{movies?.length}</Text>
</View>
</View>
{movies?.map((item: BaseItemDto) => (
<View className="mb-2 last:mb-0" key={item.Id}>
<MovieCard item={item} />
</View>
))}
</View>
{movies?.map((item: BaseItemDto) => (
<View className="mb-2 last:mb-0" key={item.Id}>
<MovieCard item={item} />
</View>
))}
</View>
<View>
{groupedBySeries?.map((items: BaseItemDto[], index: number) => (
<SeriesCard items={items} key={items[0].SeriesId} />
))}
</View>
)}
{groupedBySeries?.map((items: BaseItemDto[], index: number) => (
<SeriesCard items={items} key={items[0].SeriesId} />
))}
</View>
</ScrollView>
);

View File

@@ -16,7 +16,7 @@ import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import {
ActivityIndicator,
ScrollView,
@@ -29,6 +29,8 @@ const page: React.FC = () => {
const local = useLocalSearchParams();
const { id } = local as { id: string };
const [playbackURL, setPlaybackURL] = useState<string | null>(null);
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -148,13 +150,20 @@ const page: React.FC = () => {
</View>
<View className="flex flex-row justify-between items-center w-full my-4">
<DownloadItem item={item} />
{playbackURL && (
<DownloadItem item={item} playbackURL={playbackURL} />
)}
<Chromecast />
</View>
<Text>{item.Overview}</Text>
</View>
<View className="flex flex-col p-4">
<VideoPlayer itemId={item.Id} />
<VideoPlayer
itemId={item.Id}
onChangePlaybackURL={(val) => {
setPlaybackURL(val);
}}
/>
</View>
<ScrollView horizontal className="flex px-4 mb-4">
<View className="flex flex-row space-x-2 ">

View File

@@ -1,99 +1,22 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ListItem } from "@/components/ListItem";
import ProgressCircle from "@/components/ProgressCircle";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { runningProcesses } from "@/utils/atoms/downloads";
import { useFiles } from "@/utils/files/useFiles";
import { readFromLog } from "@/utils/log";
import { Ionicons } from "@expo/vector-icons";
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 * as FileSystem from "expo-file-system";
import { useRouter } from "expo-router";
import { FFmpegKit } from "ffmpeg-kit-react-native";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native";
const deleteAllFiles = async () => {
const directoryUri = FileSystem.documentDirectory;
try {
const fileNames = await FileSystem.readDirectoryAsync(directoryUri!);
for (let item of fileNames) {
await FileSystem.deleteAsync(`${directoryUri}/${item}`);
}
AsyncStorage.removeItem("downloaded_files");
} catch (error) {
console.error("Failed to delete the directory:", error);
}
};
const deleteFile = async (id: string | null | undefined) => {
if (!id) return;
try {
FileSystem.deleteAsync(`${FileSystem.documentDirectory}/${id}.mp4`).catch(
(err) => console.error(err)
);
const currentFiles = JSON.parse(
(await AsyncStorage.getItem("downloaded_files")) ?? "[]"
) as BaseItemDto[];
const updatedFiles = currentFiles.filter((f) => f.Id !== id);
await AsyncStorage.setItem(
"downloaded_files",
JSON.stringify(updatedFiles)
);
} catch (error) {
console.error(error);
}
};
const listDownloadedFiles = async () => {
const directoryUri = FileSystem.documentDirectory; // Directory where files are stored
try {
const fileNames = await FileSystem.readDirectoryAsync(directoryUri!);
return fileNames; // This will be an array of file names in the directory
} catch (error) {
console.error("Failed to read the directory:", error);
return [];
}
};
import { ScrollView, View } from "react-native";
import * as Haptics from "expo-haptics";
export default function settings() {
const { logout } = useJellyfin();
const { deleteAllFiles } = useFiles();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [files, setFiles] = useState<BaseItemDto[]>([]);
const [key, setKey] = useState(0);
const [session, setSession] = useAtom(runningProcesses);
const router = useRouter();
const [activeProcess] = useAtom(runningProcesses);
useEffect(() => {
(async () => {
const data = JSON.parse(
(await AsyncStorage.getItem("downloaded_files")) || "[]"
) as BaseItemDto[];
console.log(
"Files",
data.map((i) => i.Name)
);
setFiles(data);
})();
}, [key]);
const { data: logs } = useQuery({
queryKey: ["logs"],
queryFn: async () => readFromLog(),
@@ -111,99 +34,34 @@ export default function settings() {
<Button onPress={logout}>Log out</Button>
<View className="mb-4">
<Text className="font-bold text-2xl mb-4">Downloads</Text>
{files.length > 0 ? (
<View>
{files.map((file) => (
<TouchableOpacity
key={file.Id}
className="rounded-xl overflow-hidden mb-2"
onPress={() => {
router.back();
router.push(
`/(auth)/player/offline/page?url=${file.Id}.mp4&itemId=${file.Id}`
);
}}
>
<ListItem
title={file.Name}
subTitle={file.ProductionYear?.toString()}
iconAfter={
<TouchableOpacity
onPress={async () => {
await deleteFile(file.Id);
setKey((prevKey) => prevKey + 1);
}}
>
<Ionicons
name="close-circle-outline"
size={24}
color="white"
/>
</TouchableOpacity>
}
/>
</TouchableOpacity>
))}
</View>
) : activeProcess ? (
<View className="rounded-xl overflow-hidden mb-2">
<ListItem
title={activeProcess.item.Name}
iconAfter={
<ProgressCircle
size={22}
fill={activeProcess.progress}
width={3}
tintColor="#3498db"
backgroundColor="#bdc3c7"
/>
}
/>
</View>
) : (
<Text className="opacity-50">No downloaded files</Text>
)}
</View>
<Button
className="mb-2"
color="red"
onPress={async () => {
await deleteAllFiles();
setKey((prevKey) => prevKey + 1);
}}
>
Clear downloads
</Button>
{session?.item.Id && (
<View className="my-2">
<Button
className="mb-2"
onPress={() => {
FFmpegKit.cancel();
setSession(null);
color="red"
onPress={async () => {
await deleteAllFiles();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
}}
>
Cancel all downloads
Delete all downloaded files
</Button>
)}
</View>
<Text className="font-bold">Logs</Text>
<View className="flex flex-col space-y-2">
{logs?.map((l) => (
<View className="bg-neutral-800 border border-neutral-900 rounded p-2">
{logs?.map((log, index) => (
<View
key={index}
className="bg-neutral-800 border border-neutral-900 rounded p-2"
>
<Text
className={`
${l.level === "INFO" && "text-blue-500"}
${l.level === "ERROR" && "text-red-500"}
${log.level === "INFO" && "text-blue-500"}
${log.level === "ERROR" && "text-red-500"}
`}
>
{l.level}
{log.level}
</Text>
<Text>{l.message}</Text>
<Text>{log.message}</Text>
</View>
))}
</View>

View File

@@ -82,12 +82,6 @@ export default function RootLayout() {
options={{
headerShown: true,
title: "Downloads",
presentation: "modal",
headerLeft: () => (
<TouchableOpacity onPress={() => router.back()}>
<Feather name="x-circle" size={24} color="white" />
</TouchableOpacity>
),
}}
/>
<Stack.Screen
@@ -101,6 +95,7 @@ export default function RootLayout() {
<Stack.Screen
name="(auth)/items/[id]/page"
options={{
title: "",
headerShown: false,
}}
/>
@@ -116,6 +111,7 @@ export default function RootLayout() {
<Stack.Screen
name="(auth)/series/[id]/page"
options={{
title: "",
headerShown: false,
}}
/>