mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 19:18:26 +01:00
chore
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user