mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 19:18:26 +01:00
fix
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
83
app/(auth)/items/[id]/ParallaxPage.tsx
Normal file
83
app/(auth)/items/[id]/ParallaxPage.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user