From 60981504fccc549d0c4324a38c2bbea36e55a2c3 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 2 Oct 2024 22:07:13 +0200 Subject: [PATCH] wip --- app/(auth)/(tabs)/(home)/index.tsx | 71 ++++++++++-------- app/(auth)/(tabs)/(home)/settings.tsx | 1 + app/(auth)/(tabs)/(search)/index.tsx | 6 +- app/(auth)/(tabs)/_layout.tsx | 1 - app/_layout.tsx | 2 +- components/ContinueWatchingPoster.tsx | 16 +--- components/ListItem.tsx | 6 +- components/PlayedStatus.tsx | 8 +- components/common/HorrizontalScroll.tsx | 1 - components/home/ScrollingCollectionList.tsx | 81 +++++++++++++-------- components/posters/MoviePoster.tsx | 3 +- components/posters/SeriesPoster.tsx | 4 +- components/settings/SettingToggles.tsx | 40 +++++++++- constants/Values.ts | 3 + utils/optimize-server.ts | 48 +++++++++++- 15 files changed, 199 insertions(+), 92 deletions(-) create mode 100644 constants/Values.ts diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index d546190f..cc02e39e 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -4,6 +4,7 @@ import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel"; import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; import { Loader } from "@/components/Loader"; import { MediaListSection } from "@/components/medialists/MediaListSection"; +import { TAB_HEIGHT } from "@/constants/Values"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { Ionicons } from "@expo/vector-icons"; @@ -25,7 +26,6 @@ import { ActivityIndicator, Platform, RefreshControl, - SafeAreaView, ScrollView, View, } from "react-native"; @@ -139,18 +139,24 @@ export default function index() { const refetch = useCallback(async () => { setLoading(true); - await queryClient.refetchQueries({ queryKey: ["userViews"] }); - await queryClient.refetchQueries({ queryKey: ["resumeItems"] }); - await queryClient.refetchQueries({ queryKey: ["nextUp-all"] }); - await queryClient.refetchQueries({ queryKey: ["recentlyAddedInMovies"] }); - await queryClient.refetchQueries({ queryKey: ["recentlyAddedInTVShows"] }); - await queryClient.refetchQueries({ queryKey: ["suggestions"] }); - await queryClient.refetchQueries({ - queryKey: ["sf_promoted"], - }); - await queryClient.refetchQueries({ - queryKey: ["sf_carousel"], - }); + await queryClient.invalidateQueries(); + // await queryClient.invalidateQueries({ queryKey: ["userViews"] }); + // await queryClient.invalidateQueries({ queryKey: ["resumeItems"] }); + // await queryClient.invalidateQueries({ queryKey: ["continueWatching"] }); + // await queryClient.invalidateQueries({ queryKey: ["nextUp-all"] }); + // await queryClient.invalidateQueries({ + // queryKey: ["recentlyAddedInMovies"], + // }); + // await queryClient.invalidateQueries({ + // queryKey: ["recentlyAddedInTVShows"], + // }); + // await queryClient.invalidateQueries({ queryKey: ["suggestions"] }); + // await queryClient.invalidateQueries({ + // queryKey: ["sf_promoted"], + // }); + // await queryClient.invalidateQueries({ + // queryKey: ["sf_carousel"], + // }); setLoading(false); }, [queryClient, user?.Id]); @@ -344,15 +350,33 @@ export default function index() { } key={"home"} contentContainerStyle={{ + flexDirection: "column", paddingLeft: insets.left, paddingRight: insets.right, - paddingBottom: - Platform.OS === "android" ? insets.bottom + 65 : insets.bottom, + paddingTop: 8, + rowGap: 8, + }} + style={{ + marginBottom: TAB_HEIGHT, }} - className="flex flex-col space-y-4" > + + ( + await getItemsApi(api).getResumeItems({ + userId: user?.Id, + enableImageTypes: ["Primary", "Backdrop", "Thumb"], + }) + ).data.Items || [] + } + orientation={"horizontal"} + /> + - - ( - await getItemsApi(api).getResumeItems({ - userId: user?.Id, - enableImageTypes: ["Primary", "Backdrop", "Thumb"], - }) - ).data.Items || [] - } - orientation={"horizontal"} - /> - {sections.map((section, index) => { if (section.type === "ScrollingCollectionList") { return ( diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index d1fdf0f9..7077a16f 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -79,6 +79,7 @@ export default function settings() { + diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index ab64ee3c..456dae9c 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -8,6 +8,7 @@ import { Loader } from "@/components/Loader"; import AlbumCover from "@/components/posters/AlbumCover"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; +import { TAB_HEIGHT } from "@/constants/Values"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; @@ -226,8 +227,11 @@ export default function search() { paddingLeft: insets.left, paddingRight: insets.right, }} + style={{ + marginBottom: TAB_HEIGHT, + }} > - + {Platform.OS === "android" && ( = ({ item, - width = 176, useEpisodePoster = false, }) => { const [api] = useAtom(apiAtom); @@ -47,21 +47,11 @@ const ContinueWatchingPoster: React.FC = ({ if (!url) return ( - + ); return ( - + > = ({ > {title} - {subTitle && {subTitle}} + {subTitle && ( + + {subTitle} + + )} {iconAfter} diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index b3b55ee9..375cf22b 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -21,11 +21,17 @@ export const PlayedStatus: React.FC = ({ item, ...props }) => { const invalidateQueries = () => { queryClient.invalidateQueries({ - queryKey: ["item"], + queryKey: ["item", item.Id], }); queryClient.invalidateQueries({ queryKey: ["resumeItems"], }); + queryClient.invalidateQueries({ + queryKey: ["continueWatching"], + }); + queryClient.invalidateQueries({ + queryKey: ["nextUp-all"], + }); queryClient.invalidateQueries({ queryKey: ["nextUp"], }); diff --git a/components/common/HorrizontalScroll.tsx b/components/common/HorrizontalScroll.tsx index bd885fec..6679453f 100644 --- a/components/common/HorrizontalScroll.tsx +++ b/components/common/HorrizontalScroll.tsx @@ -98,7 +98,6 @@ export const HorizontalScroll = forwardRef< )} - {...props} /> ); } diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index e8420d72..433f9877 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -6,12 +6,13 @@ import { type QueryFunction, type QueryKey, } from "@tanstack/react-query"; -import { View, ViewProps } from "react-native"; +import { ScrollView, View, ViewProps } from "react-native"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { ItemCardText } from "../ItemCardText"; import { HorizontalScroll } from "../common/HorrizontalScroll"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; import SeriesPoster from "../posters/SeriesPoster"; +import { FlashList } from "@shopify/flash-list"; interface Props extends ViewProps { title?: string | null; @@ -39,40 +40,56 @@ export const ScrollingCollectionList: React.FC = ({ if (disabled || !title) return null; return ( - + {title} - ( - - {item.Type === "Episode" && orientation === "horizontal" && ( - - )} - {item.Type === "Episode" && orientation === "vertical" && ( - - )} - {item.Type === "Movie" && orientation === "horizontal" && ( - - )} - {item.Type === "Movie" && orientation === "vertical" && ( - - )} - {item.Type === "Series" && } - - - )} - /> + {isLoading ? ( + + {[1, 2, 3].map((i) => ( + + + + + + ))} + + ) : ( + + + {data?.map((item, index) => ( + + {item.Type === "Episode" && orientation === "horizontal" && ( + + )} + {item.Type === "Episode" && orientation === "vertical" && ( + + )} + {item.Type === "Movie" && orientation === "horizontal" && ( + + )} + {item.Type === "Movie" && orientation === "vertical" && ( + + )} + {item.Type === "Series" && } + + + ))} + + + )} ); }; diff --git a/components/posters/MoviePoster.tsx b/components/posters/MoviePoster.tsx index 056e2c30..46776fb7 100644 --- a/components/posters/MoviePoster.tsx +++ b/components/posters/MoviePoster.tsx @@ -36,7 +36,7 @@ const MoviePoster: React.FC = ({ }, [item]); return ( - + = ({ width: "100%", }} /> - {showProgress && progress > 0 && ( diff --git a/components/posters/SeriesPoster.tsx b/components/posters/SeriesPoster.tsx index dbadcdce..e551624a 100644 --- a/components/posters/SeriesPoster.tsx +++ b/components/posters/SeriesPoster.tsx @@ -32,7 +32,7 @@ const SeriesPoster: React.FC = ({ item }) => { }, [item]); return ( - + = ({ item }) => { cachePolicy={"memory-disk"} contentFit="cover" style={{ - aspectRatio: "10/15", + height: "100%", width: "100%", }} /> diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index 5563659e..c5ba0240 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -1,5 +1,9 @@ import { useDownload } from "@/providers/DownloadProvider"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { + apiAtom, + getOrSetDeviceId, + userAtom, +} from "@/providers/JellyfinProvider"; import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings"; import { BACKGROUND_FETCH_TASK, @@ -28,6 +32,8 @@ import { Input } from "../common/Input"; import { Text } from "../common/Text"; import { Loader } from "../Loader"; import { MediaToggles } from "./MediaToggles"; +import axios from "axios"; +import { getStatistics } from "@/utils/optimize-server"; interface Props extends ViewProps {} @@ -44,6 +50,21 @@ export const SettingToggles: React.FC = ({ ...props }) => { const queryClient = useQueryClient(); + const { data: optimizeServerStatistics } = useQuery({ + queryKey: ["optimize-server", settings?.optimizedVersionsServerUrl], + queryFn: async () => + getStatistics({ + url: settings?.optimizedVersionsServerUrl, + authHeader: api?.accessToken, + deviceId: await getOrSetDeviceId(), + }), + refetchInterval: 1000, + staleTime: 0, + enabled: + !!settings?.optimizedVersionsServerUrl && + settings.optimizedVersionsServerUrl.length > 0, + }); + /******************** * Background task *******************/ @@ -568,11 +589,24 @@ export const SettingToggles: React.FC = ({ ...props }) => { > - Optimized versions server + + + Optimized versions server + + + Set the URL for the optimized versions server for downloads. + = ({ ...props }) => { color="purple" className="h-12 mt-2" onPress={() => { - toast.success("Saved"); + toast.info("Saved"); updateSettings({ optimizedVersionsServerUrl: optimizedVersionsServerUrl.length === 0 diff --git a/constants/Values.ts b/constants/Values.ts new file mode 100644 index 00000000..4c3e4d81 --- /dev/null +++ b/constants/Values.ts @@ -0,0 +1,3 @@ +import { Platform } from "react-native"; + +export const TAB_HEIGHT = Platform.OS === "android" ? 58 : 74; diff --git a/utils/optimize-server.ts b/utils/optimize-server.ts index f52ad799..443bdf59 100644 --- a/utils/optimize-server.ts +++ b/utils/optimize-server.ts @@ -3,9 +3,9 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import axios from "axios"; interface IJobInput { - deviceId: string; - authHeader: string; - url: string; + deviceId?: string | null; + authHeader?: string | null; + url?: string | null; } export interface JobStatus { @@ -88,6 +88,10 @@ export async function cancelJobById({ } export async function cancelAllJobs({ authHeader, url, deviceId }: IJobInput) { + if (!deviceId) return false; + if (!authHeader) return false; + if (!url) return false; + try { await getAllJobsByDeviceId({ deviceId, @@ -109,3 +113,41 @@ export async function cancelAllJobs({ authHeader, url, deviceId }: IJobInput) { return true; } + +/** + * Fetches statistics for a specific device. + * + * @param {IJobInput} params - The parameters for the API request. + * @param {string} params.deviceId - The ID of the device to fetch statistics for. + * @param {string} params.authHeader - The authorization header for the API request. + * @param {string} params.url - The base URL for the API endpoint. + * + * @returns {Promise} A promise that resolves to the statistics data or null if the request fails. + * + * @throws {Error} Throws an error if any required parameter is missing. + */ +export async function getStatistics({ + authHeader, + url, + deviceId, +}: IJobInput): Promise { + if (!deviceId || !authHeader || !url) { + return null; + } + + try { + const statusResponse = await axios.get(`${url}statistics`, { + headers: { + Authorization: authHeader, + }, + params: { + deviceId, + }, + }); + + return statusResponse.data; + } catch (error) { + console.error("Failed to fetch statistics:", error); + return null; + } +}