chore: refactor

This commit is contained in:
Fredrik Burmester
2024-08-07 08:15:30 +02:00
parent d4d3cbbc43
commit 49b2e594d6
31 changed files with 525 additions and 487 deletions

View File

@@ -6,8 +6,8 @@ import { ItemCardText } from "@/components/ItemCardText";
import MoviePoster from "@/components/MoviePoster";
import Poster from "@/components/Poster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImage } from "@/utils/jellyfin";
import { getUserItemData } from "@/utils/jellyfin/items/getUserItemData";
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
@@ -117,7 +117,7 @@ export default function search() {
<Poster
item={item}
key={item.Id}
url={getPrimaryImage({ api, item })}
url={getPrimaryImageUrl({ api, item })}
/>
<Text className="mt-2">{item.Name}</Text>
<Text className="opacity-50 text-xs">

View File

@@ -6,21 +6,31 @@ 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 { ActivityIndicator, ScrollView, View } from "react-native";
import {
ActivityIndicator,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import * as FileSystem from "expo-file-system";
import { useAtom } from "jotai";
import { runningProcesses } from "@/utils/atoms/downloads";
import { router } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { FFmpegKit } from "ffmpeg-kit-react-native";
const downloads: React.FC = () => {
const { data: downloadedFiles, isLoading } = useQuery({
queryKey: ["downloaded_files"],
queryFn: async () =>
JSON.parse(
(await AsyncStorage.getItem("downloaded_files")) || "[]"
(await AsyncStorage.getItem("downloaded_files")) || "[]",
) as BaseItemDto[],
});
const movies = useMemo(
() => downloadedFiles?.filter((f) => f.Type === "Movie") || [],
[downloadedFiles]
[downloadedFiles],
);
const groupedBySeries = useMemo(() => {
@@ -39,10 +49,12 @@ const downloads: React.FC = () => {
name: i.Name,
codec: i.SourceType,
media: i.MediaSources?.[0].Container,
}))
})),
);
}, [downloadedFiles]);
const [process, setProcess] = useAtom(runningProcesses);
if (isLoading) {
return (
<View className="h-full flex flex-col items-center justify-center -mt-6">
@@ -64,6 +76,55 @@ const downloads: React.FC = () => {
return (
<ScrollView>
<View className="px-4 py-4">
<View className="mb-4">
<Text className="text-2xl font-bold mb-2">Active download</Text>
{process?.item ? (
<TouchableOpacity
onPress={() =>
router.push(`/(auth)/items/${process.item.Id}/page`)
}
className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between"
>
<View>
<Text className="font-semibold">{process.item.Name}</Text>
<Text className="text-xs opacity-50">{process.item.Type}</Text>
<View className="flex flex-row items-center space-x-2 mt-1 text-red-600">
<Text className="text-xs">
{process.progress.toFixed(0)}%
</Text>
<Text className="text-xs">{process.speed?.toFixed(2)}x</Text>
{process.startTime && (
<Text className="text-xs">
{formatNumber(
new Date().getTime() - process.startTime.getTime(),
)}
</Text>
)}
</View>
</View>
<TouchableOpacity
onPress={() => {
FFmpegKit.cancel();
setProcess(null);
}}
>
<Ionicons name="close" size={24} color="red" />
</TouchableOpacity>
<View
className={`
absolute bottom-0 left-0 h-1 bg-red-600
`}
style={{
width: process.progress
? `${Math.max(5, process.progress)}%`
: "5%",
}}
></View>
</TouchableOpacity>
) : (
<Text className="opacity-50">No active downloads</Text>
)}
</View>
{movies.length > 0 && (
<View className="mb-4">
<View className="flex flex-row items-center justify-between mb-2">
@@ -88,3 +149,15 @@ const downloads: React.FC = () => {
};
export default downloads;
/*
* Format a number (Date.getTime) to a human readable string ex. 2m 34s
* @param {number} num - The number to format
*
* @returns {string} - The formatted string
*/
const formatNumber = (num: number) => {
const minutes = Math.floor(num / 60000);
const seconds = ((num % 60000) / 1000).toFixed(0);
return `${minutes}m ${seconds}s`;
};

View File

@@ -7,7 +7,6 @@ import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SimilarItems } from "@/components/SimilarItems";
import { VideoPlayer } from "@/components/VideoPlayer";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdrop, getLogoImageById } from "@/utils/jellyfin";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router";
@@ -20,7 +19,9 @@ import {
View,
} from "react-native";
import { ParallaxScrollView } from "../../../../components/ParallaxPage";
import { getUserItemData } from "@/utils/jellyfin/items/getUserItemData";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
const page: React.FC = () => {
const local = useLocalSearchParams();
@@ -45,7 +46,7 @@ const page: React.FC = () => {
const backdropUrl = useMemo(
() =>
getBackdrop({
getBackdropUrl({
api,
item,
quality: 90,
@@ -55,7 +56,7 @@ const page: React.FC = () => {
);
const logoUrl = useMemo(
() => (item?.Type === "Movie" ? getLogoImageById({ api, item }) : null),
() => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null),
[item],
);

View File

@@ -3,8 +3,9 @@ import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdrop, getLogoImageById } from "@/utils/jellyfin";
import { getUserItemData } from "@/utils/jellyfin/items/getUserItemData";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
@@ -33,7 +34,7 @@ const page: React.FC = () => {
const backdropUrl = useMemo(
() =>
getBackdrop({
getBackdropUrl({
api,
item,
quality: 90,
@@ -44,7 +45,7 @@ const page: React.FC = () => {
const logoUrl = useMemo(
() =>
getLogoImageById({
getLogoImageUrlById({
api,
item,
}),

View File

@@ -2,7 +2,7 @@ import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { ListItem } from "@/components/ListItem";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { readFromLog } from "@/utils/log";
import { clearLogs, readFromLog } from "@/utils/log";
import { useQuery } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system";
import { useAtom } from "jotai";
@@ -20,6 +20,7 @@ export default function settings() {
const { data: logs } = useQuery({
queryKey: ["logs"],
queryFn: async () => readFromLog(),
refetchInterval: 1000,
});
return (
@@ -32,9 +33,10 @@ export default function settings() {
<ListItem title="Server" subTitle={api?.basePath} />
</View>
<Button onPress={logout}>Log out</Button>
<View className="my-2">
<View className="flex flex-col space-y-2">
<Button color="black" onPress={logout}>
Log out
</Button>
<Button
color="red"
onPress={async () => {
@@ -46,9 +48,20 @@ export default function settings() {
>
Delete all downloaded files
</Button>
<Button
color="red"
onPress={async () => {
await clearLogs();
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success,
);
}}
>
Delete all logs
</Button>
</View>
<Text className="font-bold">Logs</Text>
<Text className="font-bold text-2xl">Logs</Text>
<View className="flex flex-col space-y-2">
{logs?.map((log, index) => (
<View
@@ -66,6 +79,9 @@ export default function settings() {
<Text>{log.message}</Text>
</View>
))}
{logs?.length === 0 && (
<Text className="opacity-50">No logs available</Text>
)}
</View>
</View>
</ScrollView>

View File

@@ -6,13 +6,17 @@ import { useEffect, useRef } from "react";
import "react-native-reanimated";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Provider as JotaiProvider } from "jotai";
import { Provider as JotaiProvider, useAtom } from "jotai";
import { JellyfinProvider } from "@/providers/JellyfinProvider";
import { TouchableOpacity } from "react-native";
import Feather from "@expo/vector-icons/Feather";
import { StatusBar } from "expo-status-bar";
import { Ionicons } from "@expo/vector-icons";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { runningProcesses } from "@/utils/atoms/downloads";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
@@ -37,7 +41,7 @@ export default function RootLayout() {
retryOnMount: true,
},
},
})
}),
);
useEffect(() => {
@@ -46,6 +50,10 @@ export default function RootLayout() {
}
}, [loaded]);
const insets = useSafeAreaInsets();
const [process] = useAtom(runningProcesses);
if (!loaded) {
return null;
}