This commit is contained in:
Fredrik Burmester
2024-08-04 22:25:12 +02:00
parent 25a7edd86b
commit b3a74892c4
48 changed files with 546 additions and 449 deletions

View File

@@ -5,11 +5,13 @@ import { ItemCardText } from "@/components/ItemCardText";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi, getSuggestionsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useState } from "react";
import {
ActivityIndicator,
RefreshControl,
ScrollView,
TouchableOpacity,
View,
@@ -21,7 +23,7 @@ export default function index() {
const router = useRouter();
const { data, isLoading, isError } = useQuery<BaseItemDto[]>({
queryKey: ["resumeItems", api, user?.Id],
queryKey: ["resumeItems", user?.Id],
queryFn: async () => {
if (!api || !user?.Id) {
return [];
@@ -85,6 +87,18 @@ export default function index() {
staleTime: 60,
});
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const refetch = useCallback(async () => {
setLoading(true);
await queryClient.refetchQueries({ queryKey: ["resumeItems", user?.Id] });
await queryClient.refetchQueries({ queryKey: ["items", user?.Id] });
await queryClient.refetchQueries({ queryKey: ["suggestions", user?.Id] });
setLoading(false);
}, [queryClient, user?.Id]);
if (isError)
return (
<View className="flex flex-col items-center justify-center h-full -mt-12">
@@ -105,7 +119,12 @@ export default function index() {
if (!data || data.length === 0) return <Text>No data...</Text>;
return (
<ScrollView nestedScrollEnabled>
<ScrollView
nestedScrollEnabled
refreshControl={
<RefreshControl refreshing={loading} onRefresh={refetch} />
}
>
<View className="py-4 gap-y-2">
<Text className="px-4 text-2xl font-bold mb-2">Continue Watching</Text>
<HorizontalScroll<BaseItemDto>

View File

@@ -67,8 +67,8 @@ export default function search() {
return (
<ScrollView keyboardDismissMode="on-drag">
<View className="p-4 flex flex-col">
<View className="mb-4">
<View className="flex flex-col py-2">
<View className="mb-4 px-4">
<Input
autoCorrect={false}
returnKeyType="done"
@@ -79,7 +79,7 @@ export default function search() {
/>
</View>
<Text className="font-bold text-2xl mb-2">Movies</Text>
<Text className="font-bold text-2xl px-4 mb-2">Movies</Text>
<SearchItemWrapper
ids={movies?.map((m) => m.Id!)}
renderItem={(data) => (
@@ -101,7 +101,7 @@ export default function search() {
/>
)}
/>
<Text className="font-bold text-2xl my-2">Series</Text>
<Text className="font-bold text-2xl px-4 my-2">Series</Text>
<SearchItemWrapper
ids={series?.map((m) => m.Id!)}
renderItem={(data) => (
@@ -123,7 +123,7 @@ export default function search() {
/>
)}
/>
<Text className="font-bold text-2xl my-2">Episodes</Text>
<Text className="font-bold text-2xl px-4 my-2">Episodes</Text>
<SearchItemWrapper
ids={episodes?.map((m) => m.Id!)}
renderItem={(data) => (
@@ -195,7 +195,7 @@ const SearchItemWrapper: React.FC<Props> = ({ ids, renderItem }) => {
staleTime: Infinity,
});
if (!data) return <Text className="opacity-50 text-xs">No results</Text>;
if (!data) return <Text className="opacity-50 text-xs px-4">No results</Text>;
return renderItem(data);
};

View File

@@ -0,0 +1,83 @@
import type { PropsWithChildren, ReactElement } from "react";
import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
useColorScheme,
} from "react-native";
import Animated, {
interpolate,
useAnimatedRef,
useAnimatedStyle,
useScrollViewOffset,
} from "react-native-reanimated";
import { ThemedView } from "@/components/ThemedView";
const HEADER_HEIGHT = 250;
type Props = PropsWithChildren<{
headerImage: ReactElement;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}>;
export const ParallaxScrollView: React.FC<Props> = ({
children,
headerImage,
onScroll,
}: Props) => {
const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollViewOffset(scrollRef);
const headerAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: interpolate(
scrollOffset.value,
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
),
},
{
scale: interpolate(
scrollOffset.value,
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
[2, 1, 1]
),
},
],
};
});
return (
<ThemedView style={styles.container}>
<Animated.ScrollView ref={scrollRef} scrollEventThrottle={16}>
<Animated.View
style={[
styles.header,
{ backgroundColor: "white" },
headerAnimatedStyle,
]}
>
{headerImage}
</Animated.View>
<ThemedView style={styles.content}>{children}</ThemedView>
</Animated.ScrollView>
</ThemedView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: 250,
overflow: "hidden",
},
content: {
flex: 1,
overflow: "hidden",
},
});

View File

@@ -13,8 +13,16 @@ import {} from "@jellyfin/sdk/lib/utils/url";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { useEffect, useState } from "react";
import {
ActivityIndicator,
NativeScrollEvent,
NativeSyntheticEvent,
ScrollView,
View,
} from "react-native";
import { ParallaxScrollView } from "./ParallaxPage";
import { Image } from "expo-image";
const page: React.FC = () => {
const local = useLocalSearchParams();
@@ -23,8 +31,6 @@ const page: React.FC = () => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const navigation = useNavigation();
const { data: item, isLoading: l1 } = useQuery({
queryKey: ["item", id],
queryFn: async () =>
@@ -34,22 +40,14 @@ const page: React.FC = () => {
itemId: id,
}),
enabled: !!id && !!api,
staleTime: Infinity,
staleTime: 60,
});
useEffect(() => {
navigation.setOptions({
headerRight: () => {
<Ionicons name="accessibility" />;
},
});
}, [item, navigation]);
const { data: posterUrl } = useQuery({
queryKey: ["backdrop", item?.Id],
queryFn: async () => getBackdrop(api, item),
enabled: !!api && !!item?.Id,
staleTime: Infinity,
staleTime: 60 * 60 * 24 * 7,
});
if (l1)
@@ -59,12 +57,23 @@ const page: React.FC = () => {
</View>
);
if (!item?.Id) return null;
if (!item?.Id || !posterUrl) return null;
return (
<ScrollView style={[{ flex: 1 }]} keyboardDismissMode="on-drag">
<LargePoster url={posterUrl} />
<View className="flex flex-col px-4 mb-4">
<ParallaxScrollView
headerImage={
<Image
source={{
uri: posterUrl,
}}
style={{
width: "100%",
height: 250,
}}
/>
}
>
<View className="flex flex-col px-4 mb-4 pt-4">
<View className="flex flex-col">
{item.Type === "Episode" ? (
<>
@@ -127,12 +136,10 @@ const page: React.FC = () => {
</View>
</ScrollView>
<View className="px-4 mb-4">
<CastAndCrew item={item} />
</View>
<CastAndCrew item={item} />
{item.Type === "Episode" && (
<View className="px-4 mb-4">
<View className="mb-4">
<CurrentSeries item={item} />
</View>
)}
@@ -140,7 +147,7 @@ const page: React.FC = () => {
<SimilarItems itemId={item.Id} />
<View className="h-12"></View>
</ScrollView>
</ParallaxScrollView>
);
};

View File

@@ -18,7 +18,7 @@ const page: React.FC = () => {
const [user] = useAtom(userAtom);
const { data: item } = useQuery({
queryKey: ["item", seriesId],
queryKey: ["series", seriesId],
queryFn: async () =>
await getUserItemData({
api,
@@ -26,33 +26,23 @@ const page: React.FC = () => {
itemId: seriesId,
}),
enabled: !!seriesId && !!api,
staleTime: Infinity,
});
const { data: next } = useQuery({
queryKey: ["nextUp", seriesId],
queryFn: async () =>
await nextUp({
userId: user?.Id,
api,
itemId: seriesId,
}),
enabled: !!api && !!seriesId && !!user?.Id,
staleTime: 0,
staleTime: 60,
});
if (!item) return null;
return (
<ScrollView>
<View className="flex flex-col px-4 pt-4 pb-8">
<MoviePoster item={item} />
<View className="my-4">
<Text className="text-3xl font-bold">{item?.Name}</Text>
<Text className="">{item?.Overview}</Text>
<View className="flex flex-col pt-4 pb-8">
<View className="px-4">
<MoviePoster item={item} />
<View className="my-4">
<Text className="text-3xl font-bold">{item?.Name}</Text>
<Text className="">{item?.Overview}</Text>
</View>
</View>
<SeasonPicker item={item} />
<NextUp items={next} />
<NextUp seriesId={seriesId} />
</View>
</ScrollView>
);

View File

@@ -34,15 +34,22 @@ const deleteAllFiles = async () => {
const deleteFile = async (id: string | null | undefined) => {
if (!id) return;
FileSystem.deleteAsync(`${FileSystem.documentDirectory}/${id}.mp4`).catch(
(err) => console.error(err)
);
try {
FileSystem.deleteAsync(`${FileSystem.documentDirectory}/${id}.mp4`).catch(
(err) => console.error(err)
);
const currentFiles = JSON.parse(
(await AsyncStorage.getItem("downloaded_files")) ?? "[]"
);
const updatedFiles = currentFiles.filter((f: string) => f !== id);
await AsyncStorage.setItem("downloaded_files", JSON.stringify(updatedFiles));
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 () => {
@@ -125,8 +132,8 @@ export default function settings() {
subTitle={file.ProductionYear?.toString()}
iconAfter={
<TouchableOpacity
onPress={() => {
deleteFile(file.Id);
onPress={async () => {
await deleteFile(file.Id);
setKey((prevKey) => prevKey + 1);
}}
>
@@ -142,18 +149,20 @@ export default function settings() {
))}
</View>
) : activeProcess ? (
<ListItem
title={activeProcess.item.Name}
iconAfter={
<ProgressCircle
size={22}
fill={activeProcess.progress}
width={3}
tintColor="#3498db"
backgroundColor="#bdc3c7"
/>
}
/>
<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 File

@@ -30,12 +30,10 @@ export default function RootLayout() {
defaultOptions: {
queries: {
staleTime: 60,
refetchOnMount: false,
refetchInterval: false,
refetchIntervalInBackground: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retryOnMount: false,
refetchOnMount: true,
refetchOnReconnect: true,
refetchOnWindowFocus: true,
retryOnMount: true,
},
},
})