mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-01 23:42:22 +00:00
fix
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 ">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user