This commit is contained in:
Fredrik Burmester
2024-10-02 22:07:13 +02:00
parent 1df7d8e8fe
commit 60981504fc
15 changed files with 199 additions and 92 deletions

View File

@@ -4,6 +4,7 @@ import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel";
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList"; import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import { MediaListSection } from "@/components/medialists/MediaListSection"; import { MediaListSection } from "@/components/medialists/MediaListSection";
import { TAB_HEIGHT } from "@/constants/Values";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
@@ -25,7 +26,6 @@ import {
ActivityIndicator, ActivityIndicator,
Platform, Platform,
RefreshControl, RefreshControl,
SafeAreaView,
ScrollView, ScrollView,
View, View,
} from "react-native"; } from "react-native";
@@ -139,18 +139,24 @@ export default function index() {
const refetch = useCallback(async () => { const refetch = useCallback(async () => {
setLoading(true); setLoading(true);
await queryClient.refetchQueries({ queryKey: ["userViews"] }); await queryClient.invalidateQueries();
await queryClient.refetchQueries({ queryKey: ["resumeItems"] }); // await queryClient.invalidateQueries({ queryKey: ["userViews"] });
await queryClient.refetchQueries({ queryKey: ["nextUp-all"] }); // await queryClient.invalidateQueries({ queryKey: ["resumeItems"] });
await queryClient.refetchQueries({ queryKey: ["recentlyAddedInMovies"] }); // await queryClient.invalidateQueries({ queryKey: ["continueWatching"] });
await queryClient.refetchQueries({ queryKey: ["recentlyAddedInTVShows"] }); // await queryClient.invalidateQueries({ queryKey: ["nextUp-all"] });
await queryClient.refetchQueries({ queryKey: ["suggestions"] }); // await queryClient.invalidateQueries({
await queryClient.refetchQueries({ // queryKey: ["recentlyAddedInMovies"],
queryKey: ["sf_promoted"], // });
}); // await queryClient.invalidateQueries({
await queryClient.refetchQueries({ // queryKey: ["recentlyAddedInTVShows"],
queryKey: ["sf_carousel"], // });
}); // await queryClient.invalidateQueries({ queryKey: ["suggestions"] });
// await queryClient.invalidateQueries({
// queryKey: ["sf_promoted"],
// });
// await queryClient.invalidateQueries({
// queryKey: ["sf_carousel"],
// });
setLoading(false); setLoading(false);
}, [queryClient, user?.Id]); }, [queryClient, user?.Id]);
@@ -344,15 +350,33 @@ export default function index() {
} }
key={"home"} key={"home"}
contentContainerStyle={{ contentContainerStyle={{
flexDirection: "column",
paddingLeft: insets.left, paddingLeft: insets.left,
paddingRight: insets.right, paddingRight: insets.right,
paddingBottom: paddingTop: 8,
Platform.OS === "android" ? insets.bottom + 65 : insets.bottom, rowGap: 8,
}}
style={{
marginBottom: TAB_HEIGHT,
}} }}
className="flex flex-col space-y-4"
> >
<LargeMovieCarousel /> <LargeMovieCarousel />
<ScrollingCollectionList
key="continueWatching"
title={"Continue Watching"}
queryKey={["continueWatching", user?.Id]}
queryFn={async () =>
(
await getItemsApi(api).getResumeItems({
userId: user?.Id,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
})
).data.Items || []
}
orientation={"horizontal"}
/>
<ScrollingCollectionList <ScrollingCollectionList
key="nextUp" key="nextUp"
title={"Next Up"} title={"Next Up"}
@@ -370,21 +394,6 @@ export default function index() {
orientation={"horizontal"} orientation={"horizontal"}
/> />
<ScrollingCollectionList
key="continueWatching"
title={"Continue Watching"}
queryKey={["continueWatching", user?.Id]}
queryFn={async () =>
(
await getItemsApi(api).getResumeItems({
userId: user?.Id,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
})
).data.Items || []
}
orientation={"horizontal"}
/>
{sections.map((section, index) => { {sections.map((section, index) => {
if (section.type === "ScrollingCollectionList") { if (section.type === "ScrollingCollectionList") {
return ( return (

View File

@@ -79,6 +79,7 @@ export default function settings() {
<View className="flex flex-col rounded-xl overflow-hidden border-neutral-800 divide-y-2 divide-solid divide-neutral-800 "> <View className="flex flex-col rounded-xl overflow-hidden border-neutral-800 divide-y-2 divide-solid divide-neutral-800 ">
<ListItem title="User" subTitle={user?.Name} /> <ListItem title="User" subTitle={user?.Name} />
<ListItem title="Server" subTitle={api?.basePath} /> <ListItem title="Server" subTitle={api?.basePath} />
<ListItem title="Token" subTitle={api?.accessToken} />
</View> </View>
</View> </View>

View File

@@ -8,6 +8,7 @@ import { Loader } from "@/components/Loader";
import AlbumCover from "@/components/posters/AlbumCover"; import AlbumCover from "@/components/posters/AlbumCover";
import MoviePoster from "@/components/posters/MoviePoster"; import MoviePoster from "@/components/posters/MoviePoster";
import SeriesPoster from "@/components/posters/SeriesPoster"; import SeriesPoster from "@/components/posters/SeriesPoster";
import { TAB_HEIGHT } from "@/constants/Values";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
@@ -226,8 +227,11 @@ export default function search() {
paddingLeft: insets.left, paddingLeft: insets.left,
paddingRight: insets.right, paddingRight: insets.right,
}} }}
style={{
marginBottom: TAB_HEIGHT,
}}
> >
<View className="flex flex-col pt-4 pb-32"> <View className="flex flex-col pt-4">
{Platform.OS === "android" && ( {Platform.OS === "android" && (
<View className="mb-4 px-4"> <View className="mb-4 px-4">
<Input <Input

View File

@@ -1,6 +1,5 @@
import { TabBarIcon } from "@/components/navigation/TabBarIcon"; import { TabBarIcon } from "@/components/navigation/TabBarIcon";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useCheckRunningJobs } from "@/hooks/useCheckRunningJobs";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import * as NavigationBar from "expo-navigation-bar"; import * as NavigationBar from "expo-navigation-bar";
import { Tabs } from "expo-router"; import { Tabs } from "expo-router";

View File

@@ -245,7 +245,7 @@ function Layout() {
new QueryClient({ new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: 60 * 1000, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
refetchOnReconnect: true, refetchOnReconnect: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,

View File

@@ -5,6 +5,7 @@ import { useAtom } from "jotai";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { WatchedIndicator } from "./WatchedIndicator"; import { WatchedIndicator } from "./WatchedIndicator";
import React from "react";
type ContinueWatchingPosterProps = { type ContinueWatchingPosterProps = {
item: BaseItemDto; item: BaseItemDto;
@@ -14,7 +15,6 @@ type ContinueWatchingPosterProps = {
const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
item, item,
width = 176,
useEpisodePoster = false, useEpisodePoster = false,
}) => { }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
@@ -47,21 +47,11 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
if (!url) if (!url)
return ( return (
<View <View className="aspect-video border border-neutral-800 w-44"></View>
className="aspect-video border border-neutral-800"
style={{
width,
}}
></View>
); );
return ( return (
<View <View className="relative w-44 aspect-video rounded-lg overflow-hidden border border-neutral-800">
style={{
width,
}}
className="relative aspect-video rounded-lg overflow-hidden border border-neutral-800"
>
<Image <Image
key={item.Id} key={item.Id}
id={item.Id} id={item.Id}

View File

@@ -23,7 +23,11 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
> >
<View className="flex flex-col"> <View className="flex flex-col">
<Text className="font-bold ">{title}</Text> <Text className="font-bold ">{title}</Text>
{subTitle && <Text className="text-xs">{subTitle}</Text>} {subTitle && (
<Text className="text-xs" selectable>
{subTitle}
</Text>
)}
</View> </View>
{iconAfter} {iconAfter}
</View> </View>

View File

@@ -21,11 +21,17 @@ export const PlayedStatus: React.FC<Props> = ({ item, ...props }) => {
const invalidateQueries = () => { const invalidateQueries = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["item"], queryKey: ["item", item.Id],
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["resumeItems"], queryKey: ["resumeItems"],
}); });
queryClient.invalidateQueries({
queryKey: ["continueWatching"],
});
queryClient.invalidateQueries({
queryKey: ["nextUp-all"],
});
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["nextUp"], queryKey: ["nextUp"],
}); });

View File

@@ -98,7 +98,6 @@ export const HorizontalScroll = forwardRef<
</Text> </Text>
</View> </View>
)} )}
{...props}
/> />
); );
} }

View File

@@ -6,12 +6,13 @@ import {
type QueryFunction, type QueryFunction,
type QueryKey, type QueryKey,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { View, ViewProps } from "react-native"; import { ScrollView, View, ViewProps } from "react-native";
import ContinueWatchingPoster from "../ContinueWatchingPoster"; import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText"; import { ItemCardText } from "../ItemCardText";
import { HorizontalScroll } from "../common/HorrizontalScroll"; import { HorizontalScroll } from "../common/HorrizontalScroll";
import { TouchableItemRouter } from "../common/TouchableItemRouter"; import { TouchableItemRouter } from "../common/TouchableItemRouter";
import SeriesPoster from "../posters/SeriesPoster"; import SeriesPoster from "../posters/SeriesPoster";
import { FlashList } from "@shopify/flash-list";
interface Props extends ViewProps { interface Props extends ViewProps {
title?: string | null; title?: string | null;
@@ -39,40 +40,56 @@ export const ScrollingCollectionList: React.FC<Props> = ({
if (disabled || !title) return null; if (disabled || !title) return null;
return ( return (
<View {...props}> <View {...props} className="">
<Text className="px-4 text-lg font-bold mb-2 text-neutral-100"> <Text className="px-4 text-lg font-bold mb-2 text-neutral-100">
{title} {title}
</Text> </Text>
<HorizontalScroll {isLoading ? (
data={data} <View
extraData={[orientation, isLoading]} className={`
loading={isLoading} flex flex-row gap-2 px-4
renderItem={(item, index) => ( `}
<TouchableItemRouter >
item={item} {[1, 2, 3].map((i) => (
key={index} <View className="w-44 mb-2">
style={{ <View className="bg-neutral-800 h-24 w-full rounded-md mb-2"></View>
width: orientation === "horizontal" ? 176 : 112, <View className="bg-neutral-800 h-4 w-full rounded-md mb-2"></View>
zIndex: 100, <View className="bg-neutral-800 h-4 w-1/2 rounded-md"></View>
}} </View>
> ))}
{item.Type === "Episode" && orientation === "horizontal" && ( </View>
<ContinueWatchingPoster item={item} /> ) : (
)} <ScrollView horizontal showsHorizontalScrollIndicator={false}>
{item.Type === "Episode" && orientation === "vertical" && ( <View className="px-4 flex flex-row">
<SeriesPoster item={item} /> {data?.map((item, index) => (
)} <TouchableItemRouter
{item.Type === "Movie" && orientation === "horizontal" && ( item={item}
<ContinueWatchingPoster item={item} /> key={index}
)} className={`
{item.Type === "Movie" && orientation === "vertical" && ( mr-2
<MoviePoster item={item} />
)} ${orientation === "horizontal" ? "w-44" : "w-28"}
{item.Type === "Series" && <SeriesPoster item={item} />} `}
<ItemCardText item={item} /> >
</TouchableItemRouter> {item.Type === "Episode" && orientation === "horizontal" && (
)} <ContinueWatchingPoster item={item} />
/> )}
{item.Type === "Episode" && orientation === "vertical" && (
<SeriesPoster item={item} />
)}
{item.Type === "Movie" && orientation === "horizontal" && (
<ContinueWatchingPoster item={item} />
)}
{item.Type === "Movie" && orientation === "vertical" && (
<MoviePoster item={item} />
)}
{item.Type === "Series" && <SeriesPoster item={item} />}
<ItemCardText item={item} />
</TouchableItemRouter>
))}
</View>
</ScrollView>
)}
</View> </View>
); );
}; };

View File

@@ -36,7 +36,7 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
}, [item]); }, [item]);
return ( return (
<View className="relative rounded-lg overflow-hidden border border-neutral-900"> <View className="relative rounded-lg overflow-hidden border border-neutral-900 w-28 aspect-[10/15]">
<Image <Image
placeholder={{ placeholder={{
blurhash, blurhash,
@@ -57,7 +57,6 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
width: "100%", width: "100%",
}} }}
/> />
<WatchedIndicator item={item} /> <WatchedIndicator item={item} />
{showProgress && progress > 0 && ( {showProgress && progress > 0 && (
<View className="h-1 bg-red-600 w-full"></View> <View className="h-1 bg-red-600 w-full"></View>

View File

@@ -32,7 +32,7 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
}, [item]); }, [item]);
return ( return (
<View className="relative rounded-lg overflow-hidden border border-neutral-900"> <View className="w-28 aspect-[10/15] relative rounded-lg overflow-hidden border border-neutral-900 ">
<Image <Image
placeholder={{ placeholder={{
blurhash, blurhash,
@@ -49,7 +49,7 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
cachePolicy={"memory-disk"} cachePolicy={"memory-disk"}
contentFit="cover" contentFit="cover"
style={{ style={{
aspectRatio: "10/15", height: "100%",
width: "100%", width: "100%",
}} }}
/> />

View File

@@ -1,5 +1,9 @@
import { useDownload } from "@/providers/DownloadProvider"; 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 { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
import { import {
BACKGROUND_FETCH_TASK, BACKGROUND_FETCH_TASK,
@@ -28,6 +32,8 @@ import { Input } from "../common/Input";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { Loader } from "../Loader"; import { Loader } from "../Loader";
import { MediaToggles } from "./MediaToggles"; import { MediaToggles } from "./MediaToggles";
import axios from "axios";
import { getStatistics } from "@/utils/optimize-server";
interface Props extends ViewProps {} interface Props extends ViewProps {}
@@ -44,6 +50,21 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
const queryClient = useQueryClient(); 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 * Background task
*******************/ *******************/
@@ -568,11 +589,24 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
> >
<View className="flex flex-col bg-neutral-900 px-4 py-4"> <View className="flex flex-col bg-neutral-900 px-4 py-4">
<View className="flex flex-col shrink mb-2"> <View className="flex flex-col shrink mb-2">
<Text className="font-semibold">Optimized versions server</Text> <View className="flex flex-row justify-between items-center">
<Text className="font-semibold">
Optimized versions server
</Text>
<View
className={`
w-3 h-3 rounded-full
${
optimizeServerStatistics ? "bg-green-600" : "bg-red-600"
}
`}
></View>
</View>
<Text className="text-xs opacity-50"> <Text className="text-xs opacity-50">
Set the URL for the optimized versions server for downloads. Set the URL for the optimized versions server for downloads.
</Text> </Text>
</View> </View>
<View></View>
<View className="flex flex-col"> <View className="flex flex-col">
<Input <Input
placeholder="Optimized versions server URL..." placeholder="Optimized versions server URL..."
@@ -587,7 +621,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
color="purple" color="purple"
className="h-12 mt-2" className="h-12 mt-2"
onPress={() => { onPress={() => {
toast.success("Saved"); toast.info("Saved");
updateSettings({ updateSettings({
optimizedVersionsServerUrl: optimizedVersionsServerUrl:
optimizedVersionsServerUrl.length === 0 optimizedVersionsServerUrl.length === 0

3
constants/Values.ts Normal file
View File

@@ -0,0 +1,3 @@
import { Platform } from "react-native";
export const TAB_HEIGHT = Platform.OS === "android" ? 58 : 74;

View File

@@ -3,9 +3,9 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import axios from "axios"; import axios from "axios";
interface IJobInput { interface IJobInput {
deviceId: string; deviceId?: string | null;
authHeader: string; authHeader?: string | null;
url: string; url?: string | null;
} }
export interface JobStatus { export interface JobStatus {
@@ -88,6 +88,10 @@ export async function cancelJobById({
} }
export async function cancelAllJobs({ authHeader, url, deviceId }: IJobInput) { export async function cancelAllJobs({ authHeader, url, deviceId }: IJobInput) {
if (!deviceId) return false;
if (!authHeader) return false;
if (!url) return false;
try { try {
await getAllJobsByDeviceId({ await getAllJobsByDeviceId({
deviceId, deviceId,
@@ -109,3 +113,41 @@ export async function cancelAllJobs({ authHeader, url, deviceId }: IJobInput) {
return true; 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<any | null>} 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<any | null> {
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;
}
}