This commit is contained in:
Fredrik Burmester
2024-08-06 08:18:17 +02:00
parent 2aa30ab4ca
commit 382e70cf8e
55 changed files with 4135 additions and 397 deletions

View File

@@ -19,6 +19,7 @@ export default function TabLayout() {
options={{
headerShown: true,
headerStyle: { backgroundColor: "black" },
headerShadowVisible: false,
title: "Home",
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
@@ -43,6 +44,7 @@ export default function TabLayout() {
options={{
headerStyle: { backgroundColor: "black" },
headerShown: true,
headerShadowVisible: false,
title: "Search",
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name={focused ? "search" : "search"} color={color} />

View File

@@ -3,12 +3,13 @@ import { Text } from "@/components/common/Text";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { ItemCardText } from "@/components/ItemCardText";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { nextUp } from "@/utils/jellyfin";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi, getSuggestionsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import {
ActivityIndicator,
RefreshControl,
@@ -38,6 +39,21 @@ export default function index() {
staleTime: 60,
});
const { data: _nextUpData } = useQuery({
queryKey: ["nextUp-all", user?.Id],
queryFn: async () =>
await nextUp({
userId: user?.Id,
api,
}),
enabled: !!api && !!user?.Id,
staleTime: 0,
});
const nextUpData = useMemo(() => {
return _nextUpData?.filter((i) => !data?.find((d) => d.Id === i.Id));
}, [_nextUpData]);
const { data: collections } = useQuery({
queryKey: ["collections", user?.Id],
queryFn: async () => {
@@ -142,6 +158,22 @@ export default function index() {
</TouchableOpacity>
)}
/>
<Text className="px-4 text-2xl font-bold mb-2">Next Up</Text>
<HorizontalScroll<BaseItemDto>
data={nextUpData}
renderItem={(item, index) => (
<TouchableOpacity
key={index}
onPress={() => router.push(`/items/${item.Id}/page`)}
className="flex flex-col w-48"
>
<View>
<ContinueWatchingPoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableOpacity>
)}
/>
<Text className="px-4 text-2xl font-bold mb-2">Collections</Text>
<HorizontalScroll<BaseItemDto>
data={collections}

View File

@@ -6,7 +6,7 @@ import { ItemCardText } from "@/components/ItemCardText";
import MoviePoster from "@/components/MoviePoster";
import Poster from "@/components/Poster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin";
import { getPrimaryImage, getUserItemData } from "@/utils/jellyfin";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
@@ -113,7 +113,11 @@ export default function search() {
onPress={() => router.push(`/series/${item.Id}/page`)}
className="flex flex-col w-32"
>
<Poster itemId={item.Id} key={item.Id} />
<Poster
item={item}
key={item.Id}
url={getPrimaryImage({ api, item })}
/>
<Text className="mt-2">{item.Name}</Text>
<Text className="opacity-50 text-xs">
{item.ProductionYear}

View File

@@ -1,83 +0,0 @@
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

@@ -1,4 +1,3 @@
import { LargePoster } from "@/components/common/LargePoster";
import { Text } from "@/components/common/Text";
import { DownloadItem } from "@/components/DownloadItem";
import { PlayedStatus } from "@/components/PlayedStatus";
@@ -7,22 +6,26 @@ import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SimilarItems } from "@/components/SimilarItems";
import { VideoPlayer } from "@/components/VideoPlayer";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getBackdrop, getStreamUrl, getUserItemData } from "@/utils/jellyfin";
import { Ionicons } from "@expo/vector-icons";
import {} from "@jellyfin/sdk/lib/utils/url";
import {
getBackdrop,
getLogoImageById,
getPrimaryImage,
getUserItemData,
} from "@/utils/jellyfin";
import { useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useMemo } from "react";
import {
ActivityIndicator,
NativeScrollEvent,
NativeSyntheticEvent,
ScrollView,
TouchableOpacity,
View,
} from "react-native";
import { ParallaxScrollView } from "./ParallaxPage";
import { Image } from "expo-image";
import { ParallaxScrollView } from "../../../../components/ParallaxPage";
import { Chromecast } from "@/components/Chromecast";
import { useRemoteMediaClient } from "react-native-google-cast";
const page: React.FC = () => {
const local = useLocalSearchParams();
@@ -43,12 +46,21 @@ const page: React.FC = () => {
staleTime: 60,
});
const { data: posterUrl } = useQuery({
queryKey: ["backdrop", item?.Id],
queryFn: async () => getBackdrop(api, item),
enabled: !!api && !!item?.Id,
staleTime: 60 * 60 * 24 * 7,
});
const backdropUrl = useMemo(
() =>
getBackdrop({
api,
item,
quality: 90,
width: 1000,
}),
[item]
);
const logoUrl = useMemo(
() => (item?.Type === "Movie" ? getLogoImageById({ api, item }) : null),
[item]
);
if (l1)
return (
@@ -57,35 +69,71 @@ const page: React.FC = () => {
</View>
);
if (!item?.Id || !posterUrl) return null;
if (!item?.Id || !backdropUrl) return null;
return (
<ParallaxScrollView
headerImage={
<Image
source={{
uri: posterUrl,
uri: backdropUrl,
}}
style={{
width: "100%",
height: 250,
height: "100%",
}}
/>
}
logo={
<>
{logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
/>
) : null}
</>
}
>
<View className="flex flex-col px-4 mb-4 pt-4">
<View className="flex flex-col">
{item.Type === "Episode" ? (
<>
<Text className="text-center opacity-50">{item?.SeriesName}</Text>
<Text className="text-center font-bold text-2xl">
{item?.Name}
</Text>
<TouchableOpacity
onPress={() =>
router.push(`/(auth)/series/${item.SeriesId}/page`)
}
>
<Text className="text-center opacity-50">
{item?.SeriesName}
</Text>
</TouchableOpacity>
<View className="flex flex-row items-center self-center">
<Text className="text-center font-bold text-2xl mr-2">
{item?.Name}
</Text>
<PlayedStatus item={item} />
</View>
<View>
<View className="flex flex-row items-center self-center">
<TouchableOpacity onPress={() => {}}>
<Text className="text-center opacity-50">
{item?.SeasonName}
</Text>
</TouchableOpacity>
<Text className="text-center opacity-50 mx-2">{"—"}</Text>
<Text className="text-center opacity-50">
{`Episode ${item.IndexNumber}`}
</Text>
</View>
</View>
<Text className="text-center opacity-50">
{`S${item?.SeasonName?.replace("Season ", "")}:E${(
item.IndexNumber || 0
).toString()}`}
{" - "}
{item.ProductionYear}
</Text>
</>
@@ -101,11 +149,9 @@ const page: React.FC = () => {
)}
</View>
<View className="flex flex-row justify-center items-center w-full my-4 space-x-4">
<View className="flex flex-row justify-between items-center w-full my-4">
<DownloadItem item={item} />
<View className="ml-4">
<PlayedStatus item={item} />
</View>
<Chromecast />
</View>
<Text>{item.Overview}</Text>
</View>

View File

@@ -1,14 +1,21 @@
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/MoviePoster";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { NextUp } from "@/components/series/NextUp";
import { SeasonPicker } from "@/components/series/SeasonPicker";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getPrimaryImageById, getUserItemData, nextUp } from "@/utils/jellyfin";
import {
getBackdrop,
getLogoImageById,
getPrimaryImage,
getPrimaryImageById,
getUserItemData,
} from "@/utils/jellyfin";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { ScrollView, View } from "react-native";
import { useMemo } from "react";
import { View } from "react-native";
const page: React.FC = () => {
const params = useLocalSearchParams();
@@ -26,25 +33,72 @@ const page: React.FC = () => {
itemId: seriesId,
}),
enabled: !!seriesId && !!api,
staleTime: 60,
staleTime: 0,
});
if (!item) return null;
const backdropUrl = useMemo(
() =>
getBackdrop({
api,
item,
quality: 90,
width: 1000,
}),
[item]
);
const logoUrl = useMemo(
() =>
getLogoImageById({
api,
item,
}),
[item]
);
if (!item || !backdropUrl) return null;
return (
<ScrollView>
<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>
<ParallaxScrollView
headerImage={
<Image
source={{
uri: backdropUrl,
}}
style={{
width: "100%",
height: "100%",
}}
/>
}
logo={
<>
{logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
/>
) : null}
</>
}
>
<View className="flex flex-col pt-4 pb-12">
<View className="px-4 py-4">
<Text className="text-3xl font-bold">{item?.Name}</Text>
<Text className="">{item?.Overview}</Text>
</View>
<View className="mb-4">
<NextUp seriesId={seriesId} />
</View>
<SeasonPicker item={item} />
<NextUp seriesId={seriesId} />
</View>
</ScrollView>
</ParallaxScrollView>
);
};

View File

@@ -12,6 +12,7 @@ import { TouchableOpacity } from "react-native";
import Feather from "@expo/vector-icons/Feather";
import { StatusBar } from "expo-status-bar";
import { Ionicons } from "@expo/vector-icons";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
@@ -87,10 +88,7 @@ export default function RootLayout() {
<Stack.Screen
name="(auth)/items/[id]/page"
options={{
title: "",
headerShown: true,
headerStyle: { backgroundColor: "transparent" },
headerShadowVisible: true,
headerShown: false,
}}
/>
<Stack.Screen
@@ -99,16 +97,13 @@ export default function RootLayout() {
title: "",
headerShown: true,
headerStyle: { backgroundColor: "transparent" },
headerShadowVisible: true,
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="(auth)/series/[id]/page"
options={{
title: "",
headerShown: true,
headerStyle: { backgroundColor: "transparent" },
headerShadowVisible: true,
headerShown: false,
}}
/>
<Stack.Screen