mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
wip
This commit is contained in:
@@ -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"
|
||||
>
|
||||
<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
|
||||
key="nextUp"
|
||||
title={"Next Up"}
|
||||
@@ -370,21 +394,6 @@ export default function index() {
|
||||
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) => {
|
||||
if (section.type === "ScrollingCollectionList") {
|
||||
return (
|
||||
|
||||
@@ -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 ">
|
||||
<ListItem title="User" subTitle={user?.Name} />
|
||||
<ListItem title="Server" subTitle={api?.basePath} />
|
||||
<ListItem title="Token" subTitle={api?.accessToken} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-col pt-4 pb-32">
|
||||
<View className="flex flex-col pt-4">
|
||||
{Platform.OS === "android" && (
|
||||
<View className="mb-4 px-4">
|
||||
<Input
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { useCheckRunningJobs } from "@/hooks/useCheckRunningJobs";
|
||||
import { BlurView } from "expo-blur";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { Tabs } from "expo-router";
|
||||
|
||||
@@ -245,7 +245,7 @@ function Layout() {
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
staleTime: 0,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAtom } from "jotai";
|
||||
import { useMemo, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { WatchedIndicator } from "./WatchedIndicator";
|
||||
import React from "react";
|
||||
|
||||
type ContinueWatchingPosterProps = {
|
||||
item: BaseItemDto;
|
||||
@@ -14,7 +15,6 @@ type ContinueWatchingPosterProps = {
|
||||
|
||||
const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
|
||||
item,
|
||||
width = 176,
|
||||
useEpisodePoster = false,
|
||||
}) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
@@ -47,21 +47,11 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
|
||||
|
||||
if (!url)
|
||||
return (
|
||||
<View
|
||||
className="aspect-video border border-neutral-800"
|
||||
style={{
|
||||
width,
|
||||
}}
|
||||
></View>
|
||||
<View className="aspect-video border border-neutral-800 w-44"></View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width,
|
||||
}}
|
||||
className="relative aspect-video rounded-lg overflow-hidden border border-neutral-800"
|
||||
>
|
||||
<View className="relative w-44 aspect-video rounded-lg overflow-hidden border border-neutral-800">
|
||||
<Image
|
||||
key={item.Id}
|
||||
id={item.Id}
|
||||
|
||||
@@ -23,7 +23,11 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
|
||||
>
|
||||
<View className="flex flex-col">
|
||||
<Text className="font-bold ">{title}</Text>
|
||||
{subTitle && <Text className="text-xs">{subTitle}</Text>}
|
||||
{subTitle && (
|
||||
<Text className="text-xs" selectable>
|
||||
{subTitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{iconAfter}
|
||||
</View>
|
||||
|
||||
@@ -21,11 +21,17 @@ export const PlayedStatus: React.FC<Props> = ({ 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"],
|
||||
});
|
||||
|
||||
@@ -98,7 +98,6 @@ export const HorizontalScroll = forwardRef<
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,22 +40,36 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
||||
if (disabled || !title) return null;
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<View {...props} className="">
|
||||
<Text className="px-4 text-lg font-bold mb-2 text-neutral-100">
|
||||
{title}
|
||||
</Text>
|
||||
<HorizontalScroll
|
||||
data={data}
|
||||
extraData={[orientation, isLoading]}
|
||||
loading={isLoading}
|
||||
renderItem={(item, index) => (
|
||||
{isLoading ? (
|
||||
<View
|
||||
className={`
|
||||
flex flex-row gap-2 px-4
|
||||
`}
|
||||
>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<View className="w-44 mb-2">
|
||||
<View className="bg-neutral-800 h-24 w-full rounded-md mb-2"></View>
|
||||
<View className="bg-neutral-800 h-4 w-full rounded-md mb-2"></View>
|
||||
<View className="bg-neutral-800 h-4 w-1/2 rounded-md"></View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View className="px-4 flex flex-row">
|
||||
{data?.map((item, index) => (
|
||||
<TouchableItemRouter
|
||||
item={item}
|
||||
key={index}
|
||||
style={{
|
||||
width: orientation === "horizontal" ? 176 : 112,
|
||||
zIndex: 100,
|
||||
}}
|
||||
className={`
|
||||
mr-2
|
||||
|
||||
${orientation === "horizontal" ? "w-44" : "w-28"}
|
||||
`}
|
||||
>
|
||||
{item.Type === "Episode" && orientation === "horizontal" && (
|
||||
<ContinueWatchingPoster item={item} />
|
||||
@@ -71,8 +86,10 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
||||
{item.Type === "Series" && <SeriesPoster item={item} />}
|
||||
<ItemCardText item={item} />
|
||||
</TouchableItemRouter>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
|
||||
}, [item]);
|
||||
|
||||
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
|
||||
placeholder={{
|
||||
blurhash,
|
||||
@@ -57,7 +57,6 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
|
||||
<WatchedIndicator item={item} />
|
||||
{showProgress && progress > 0 && (
|
||||
<View className="h-1 bg-red-600 w-full"></View>
|
||||
|
||||
@@ -32,7 +32,7 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
|
||||
}, [item]);
|
||||
|
||||
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
|
||||
placeholder={{
|
||||
blurhash,
|
||||
@@ -49,7 +49,7 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
aspectRatio: "10/15",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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> = ({ ...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> = ({ ...props }) => {
|
||||
>
|
||||
<View className="flex flex-col bg-neutral-900 px-4 py-4">
|
||||
<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">
|
||||
Set the URL for the optimized versions server for downloads.
|
||||
</Text>
|
||||
</View>
|
||||
<View></View>
|
||||
<View className="flex flex-col">
|
||||
<Input
|
||||
placeholder="Optimized versions server URL..."
|
||||
@@ -587,7 +621,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||
color="purple"
|
||||
className="h-12 mt-2"
|
||||
onPress={() => {
|
||||
toast.success("Saved");
|
||||
toast.info("Saved");
|
||||
updateSettings({
|
||||
optimizedVersionsServerUrl:
|
||||
optimizedVersionsServerUrl.length === 0
|
||||
|
||||
3
constants/Values.ts
Normal file
3
constants/Values.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export const TAB_HEIGHT = Platform.OS === "android" ? 58 : 74;
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user