mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-03 04:28:31 +01:00
feat: guide grid view
This commit is contained in:
@@ -169,7 +169,7 @@ export default function index() {
|
||||
setLoading(true);
|
||||
await queryClient.invalidateQueries();
|
||||
setLoading(false);
|
||||
}, [queryClient, user?.Id]);
|
||||
}, []);
|
||||
|
||||
const createCollectionConfig = useCallback(
|
||||
(
|
||||
|
||||
@@ -1,15 +1,96 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ItemContent } from "@/components/ItemContent";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import React from "react";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { opacity } from "react-native-reanimated/lib/typescript/Colors";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const { id } = useLocalSearchParams() as { id: string };
|
||||
|
||||
const { data: item, isError } = useQuery({
|
||||
queryKey: ["item", id],
|
||||
queryFn: async () => {
|
||||
const res = await getUserItemData({
|
||||
api,
|
||||
userId: user?.Id,
|
||||
itemId: id,
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
enabled: !!id && !!api,
|
||||
staleTime: 60 * 1000 * 5, // 5 minutes
|
||||
});
|
||||
|
||||
const opacity = useSharedValue(1);
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: opacity.value,
|
||||
};
|
||||
});
|
||||
|
||||
const fadeOut = (callback: any) => {
|
||||
opacity.value = withTiming(0, { duration: 300 }, (finished) => {
|
||||
if (finished) {
|
||||
runOnJS(callback)();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const fadeIn = (callback: any) => {
|
||||
opacity.value = withTiming(1, { duration: 300 }, (finished) => {
|
||||
if (finished) {
|
||||
runOnJS(callback)();
|
||||
}
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
fadeOut(() => {});
|
||||
} else {
|
||||
fadeIn(() => {});
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
if (isError)
|
||||
return (
|
||||
<View className="flex flex-col items-center justify-center h-screen w-screen">
|
||||
<Text>Could not load item</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ autoHideHomeIndicator: true }} />
|
||||
<ItemContent id={id} />
|
||||
</>
|
||||
<View className="flex flex-1 relative">
|
||||
<Animated.View
|
||||
pointerEvents={"none"}
|
||||
style={[animatedStyle]}
|
||||
className="absolute top-0 left-0 flex flex-col items-start h-screen w-screen px-4 z-50 bg-black"
|
||||
>
|
||||
<View className="h-[350px] bg-transparent rounded-lg mb-4 w-full"></View>
|
||||
<View className="h-6 bg-neutral-900 rounded mb-1 w-12"></View>
|
||||
<View className="h-12 bg-neutral-900 rounded-lg mb-1 w-1/2"></View>
|
||||
<View className="h-12 bg-neutral-900 rounded-lg w-2/3 mb-10"></View>
|
||||
<View className="h-4 bg-neutral-900 rounded-lg mb-1 w-full"></View>
|
||||
<View className="h-12 bg-neutral-900 rounded-lg mb-1 w-full"></View>
|
||||
<View className="h-12 bg-neutral-900 rounded-lg mb-1 w-full"></View>
|
||||
<View className="h-4 bg-neutral-900 rounded-lg mb-1 w-1/4"></View>
|
||||
</Animated.View>
|
||||
{item && <ItemContent item={item} />}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,142 @@
|
||||
import { ItemImage } from "@/components/common/ItemImage";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { HourHeader } from "@/components/livetv/HourHeader";
|
||||
import { LiveTVGuideRow } from "@/components/livetv/LiveTVGuideRow";
|
||||
import { TAB_HEIGHT } from "@/constants/Values";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemDtoQueryResult,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { getLiveTvApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useState } from "react";
|
||||
import { Dimensions, ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const HOUR_HEIGHT = 30
|
||||
|
||||
export default function page() {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const insets = useSafeAreaInsets();
|
||||
const [date, setDate] = useState<Date>(new Date());
|
||||
|
||||
const { data: guideInfo } = useQuery({
|
||||
queryKey: ["livetv", "guideInfo"],
|
||||
queryFn: async () => {
|
||||
const res = await getLiveTvApi(api!).getGuideInfo();
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
const { data: channels } = useQuery({
|
||||
queryKey: ["livetv", "channels"],
|
||||
queryFn: async () => {
|
||||
const res = await getLiveTvApi(api!).getLiveTvChannels({
|
||||
startIndex: 0,
|
||||
limit: 500,
|
||||
enableFavoriteSorting: true,
|
||||
userId: user?.Id,
|
||||
addCurrentProgram: false,
|
||||
enableUserData: false,
|
||||
enableImageTypes: ["Primary"],
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: programs } = useQuery({
|
||||
queryKey: ["livetv", "programs", date],
|
||||
queryFn: async () => {
|
||||
const startOfDay = new Date(date);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(date);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
|
||||
const now = new Date();
|
||||
const isToday = startOfDay.toDateString() === now.toDateString();
|
||||
|
||||
const res = await getLiveTvApi(api!).getPrograms({
|
||||
getProgramsDto: {
|
||||
MaxStartDate: endOfDay.toISOString(),
|
||||
MinEndDate: isToday ? now.toISOString() : startOfDay.toISOString(),
|
||||
ChannelIds: channels?.Items?.map((c) => c.Id).filter(
|
||||
Boolean
|
||||
) as string[],
|
||||
ImageTypeLimit: 1,
|
||||
EnableImages: false,
|
||||
SortBy: ["StartDate"],
|
||||
EnableTotalRecordCount: false,
|
||||
EnableUserData: false,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!channels,
|
||||
});
|
||||
|
||||
const screenWidth = Dimensions.get("window").width;
|
||||
|
||||
const [scrollX, setScrollX] = useState(0);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Not implemented</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
key={"home"}
|
||||
contentContainerStyle={{
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: 16,
|
||||
}}
|
||||
style={{
|
||||
marginBottom: TAB_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-row">
|
||||
<View className="flex flex-col w-[64px]">
|
||||
<View
|
||||
style={{
|
||||
height: HOUR_HEIGHT,
|
||||
}}
|
||||
></View>
|
||||
{channels?.Items?.map((c, i) => (
|
||||
<View className="h-16 w-16 mr-4 rounded-lg overflow-hidden">
|
||||
<ItemImage
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
resizeMode: "contain",
|
||||
}}
|
||||
item={c}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<ScrollView
|
||||
style={{
|
||||
width: screenWidth - 64,
|
||||
}}
|
||||
horizontal
|
||||
scrollEnabled
|
||||
onScroll={(e) => {
|
||||
setScrollX(e.nativeEvent.contentOffset.x);
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-col">
|
||||
<HourHeader height={HOUR_HEIGHT} />
|
||||
{channels?.Items?.map((c, i) => (
|
||||
<LiveTVGuideRow
|
||||
channel={c}
|
||||
programs={programs?.Items}
|
||||
key={c.Id}
|
||||
scrollX={scrollX}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -337,6 +337,7 @@ function Layout() {
|
||||
name="(auth)/play"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
title: "",
|
||||
animation: "fade",
|
||||
}}
|
||||
@@ -345,6 +346,7 @@ function Layout() {
|
||||
name="(auth)/play-music"
|
||||
options={{
|
||||
headerShown: false,
|
||||
autoHideHomeIndicator: true,
|
||||
title: "",
|
||||
animation: "fade",
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user