mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
Merge branch 'develop' into feature/bigscreen
This commit is contained in:
@@ -223,12 +223,14 @@ export default function page() {
|
||||
mediaType={item.mediaType as "movie" | "tv"}
|
||||
/>
|
||||
{/*<Text numberOfLines={1}>{item.title}</Text>*/}
|
||||
<Text
|
||||
className="text-xs opacity-50 align-bottom"
|
||||
numberOfLines={1}
|
||||
>
|
||||
as {item.character}
|
||||
</Text>
|
||||
{item.character && (
|
||||
<Text
|
||||
className="text-xs opacity-50 align-bottom mt-1"
|
||||
numberOfLines={1}
|
||||
>
|
||||
as {item.character}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
|
||||
@@ -42,22 +42,18 @@ import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
|
||||
import DetailFacts from "@/components/jellyseerr/DetailFacts";
|
||||
import { ItemActions } from "@/components/series/SeriesActions";
|
||||
import Cast from "@/components/jellyseerr/Cast";
|
||||
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const params = useLocalSearchParams();
|
||||
const {
|
||||
mediaTitle,
|
||||
releaseYear,
|
||||
canRequest: canRequestString,
|
||||
posterSrc,
|
||||
...result
|
||||
} = params as unknown as {
|
||||
mediaTitle: string;
|
||||
releaseYear: number;
|
||||
canRequest: string;
|
||||
posterSrc: string;
|
||||
} & Partial<MovieResult | TvResult>;
|
||||
const { mediaTitle, releaseYear, posterSrc, ...result } =
|
||||
params as unknown as {
|
||||
mediaTitle: string;
|
||||
releaseYear: number;
|
||||
canRequest: string;
|
||||
posterSrc: string;
|
||||
} & Partial<MovieResult | TvResult>;
|
||||
|
||||
const navigation = useNavigation();
|
||||
const { jellyseerrApi, requestMedia } = useJellyseerr();
|
||||
@@ -87,19 +83,7 @@ const Page: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const canRequest = useMemo(() => {
|
||||
const pendingRequests = details?.mediaInfo?.requests?.some(
|
||||
(r: MediaRequest) =>
|
||||
r.status == MediaRequestStatus.PENDING ||
|
||||
r.status == MediaRequestStatus.APPROVED
|
||||
);
|
||||
|
||||
return (
|
||||
(details?.mediaInfo?.status === MediaStatus.UNKNOWN &&
|
||||
!pendingRequests) ||
|
||||
(!details?.mediaInfo?.status && canRequestString === "true")
|
||||
);
|
||||
}, [canRequestString, details]);
|
||||
const canRequest = useJellyseerrCanRequest(details);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
@@ -225,7 +209,9 @@ const Page: React.FC = () => {
|
||||
<View className="mb-4">
|
||||
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
|
||||
</View>
|
||||
{canRequest ? (
|
||||
{isLoading || isFetching ? (
|
||||
<Button loading={true} disabled={true} color="purple"></Button>
|
||||
) : canRequest ? (
|
||||
<Button color="purple" onPress={request}>
|
||||
Request
|
||||
</Button>
|
||||
@@ -260,7 +246,7 @@ const Page: React.FC = () => {
|
||||
className="p-2 border border-neutral-800 bg-neutral-900 rounded-xl"
|
||||
details={details}
|
||||
/>
|
||||
<Cast className="px-4" details={details} />
|
||||
<Cast details={details} />
|
||||
</View>
|
||||
</View>
|
||||
</ParallaxScrollView>
|
||||
|
||||
@@ -31,13 +31,18 @@ import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import {MovieResult, PersonResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||
import {
|
||||
MovieResult,
|
||||
PersonResult,
|
||||
TvResult,
|
||||
} from "@/utils/jellyseerr/server/models/Search";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||
import { Tag } from "@/components/GenreTags";
|
||||
import DiscoverSlide from "@/components/jellyseerr/DiscoverSlide";
|
||||
import { sortBy } from "lodash";
|
||||
import PersonPoster from "@/components/jellyseerr/PersonPoster";
|
||||
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
|
||||
|
||||
type SearchType = "Library" | "Discover";
|
||||
|
||||
@@ -150,8 +155,8 @@ export default function search() {
|
||||
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||
});
|
||||
|
||||
const { data: jellyseerrResults, isFetching: j1 } = useQuery({
|
||||
queryKey: ["search", "jellyseerrResults", debouncedSearch],
|
||||
const { data: jellyseerrResults, isFetching: j1 } = useReactNavigationQuery({
|
||||
queryKey: ["search", "jellyseerr", "results", debouncedSearch],
|
||||
queryFn: async () => {
|
||||
const response = await jellyseerrApi?.search({
|
||||
query: new URLSearchParams(debouncedSearch).toString(),
|
||||
@@ -167,14 +172,15 @@ export default function search() {
|
||||
debouncedSearch.length > 0,
|
||||
});
|
||||
|
||||
const { data: jellyseerrDiscoverSettings, isFetching: j2 } = useQuery({
|
||||
queryKey: ["search", "jellyseerrDiscoverSettings", debouncedSearch],
|
||||
queryFn: async () => jellyseerrApi?.discoverSettings(),
|
||||
enabled:
|
||||
!!jellyseerrApi &&
|
||||
searchType === "Discover" &&
|
||||
debouncedSearch.length == 0,
|
||||
});
|
||||
const { data: jellyseerrDiscoverSettings, isFetching: j2 } =
|
||||
useReactNavigationQuery({
|
||||
queryKey: ["search", "jellyseerr", "discoverSettings", debouncedSearch],
|
||||
queryFn: async () => jellyseerrApi?.discoverSettings(),
|
||||
enabled:
|
||||
!!jellyseerrApi &&
|
||||
searchType === "Discover" &&
|
||||
debouncedSearch.length == 0,
|
||||
});
|
||||
|
||||
const jellyseerrMovieResults: MovieResult[] | undefined = useMemo(
|
||||
() =>
|
||||
@@ -309,7 +315,7 @@ export default function search() {
|
||||
paddingRight: insets.right,
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-col pt-2">
|
||||
<View className="flex flex-col">
|
||||
{Platform.OS === "android" && (
|
||||
<View className="mb-4 px-4">
|
||||
<Input
|
||||
|
||||
@@ -61,7 +61,9 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader />
|
||||
<View className="p-0.5">
|
||||
<Loader />
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
className={`
|
||||
|
||||
@@ -13,7 +13,7 @@ const CastSlide: React.FC<
|
||||
details?.credits?.cast?.length &&
|
||||
details?.credits?.cast?.length > 0 && (
|
||||
<View {...props}>
|
||||
<Text className="text-lg font-bold mb-2">Cast</Text>
|
||||
<Text className="text-lg font-bold mb-2 px-4">Cast</Text>
|
||||
<FlashList
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
@@ -21,6 +21,7 @@ const CastSlide: React.FC<
|
||||
ItemSeparatorComponent={() => <View className="w-2" />}
|
||||
estimatedItemSize={15}
|
||||
keyExtractor={(item) => item?.id?.toString()}
|
||||
contentContainerStyle={{ paddingHorizontal: 16 }}
|
||||
renderItem={({ item }) => (
|
||||
<PersonPoster
|
||||
id={item.id.toString()}
|
||||
|
||||
@@ -1,53 +1,47 @@
|
||||
import {View, ViewProps} from "react-native";
|
||||
import {Image} from "expo-image";
|
||||
import {Text} from "@/components/common/Text";
|
||||
import {useMemo} from "react";
|
||||
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||
import {MediaStatus, MediaType} from "@/utils/jellyseerr/server/constants/media";
|
||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import {hasPermission, Permission} from "@/utils/jellyseerr/server/lib/permissions";
|
||||
import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import { Image } from "expo-image";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useMemo } from "react";
|
||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import {
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from "@/utils/jellyseerr/server/constants/media";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import {
|
||||
hasPermission,
|
||||
Permission,
|
||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter";
|
||||
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
|
||||
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
|
||||
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
|
||||
interface Props extends ViewProps {
|
||||
item: MovieResult | TvResult;
|
||||
}
|
||||
|
||||
const JellyseerrPoster: React.FC<Props> = ({
|
||||
item,
|
||||
...props
|
||||
}) => {
|
||||
const {jellyseerrUser, jellyseerrApi} = useJellyseerr();
|
||||
// const imageSource =
|
||||
const JellyseerrPoster: React.FC<Props> = ({ item, ...props }) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
|
||||
const imageSrc = useMemo(
|
||||
() => jellyseerrApi?.imageProxy(item.posterPath, 'w300_and_h450_face'),
|
||||
() => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"),
|
||||
[item, jellyseerrApi]
|
||||
)
|
||||
const title = useMemo(() => item.mediaType === MediaType.MOVIE ? item.title : item.name, [item])
|
||||
const releaseYear = useMemo(() =>
|
||||
new Date(item.mediaType === MediaType.MOVIE ? item.releaseDate : item.firstAirDate).getFullYear(),
|
||||
);
|
||||
const title = useMemo(
|
||||
() => (item.mediaType === MediaType.MOVIE ? item.title : item.name),
|
||||
[item]
|
||||
)
|
||||
);
|
||||
const releaseYear = useMemo(
|
||||
() =>
|
||||
new Date(
|
||||
item.mediaType === MediaType.MOVIE
|
||||
? item.releaseDate
|
||||
: item.firstAirDate
|
||||
).getFullYear(),
|
||||
[item]
|
||||
);
|
||||
|
||||
const showRequestButton = useMemo(() =>
|
||||
jellyseerrUser && hasPermission(
|
||||
[
|
||||
Permission.REQUEST,
|
||||
item.mediaType === 'movie'
|
||||
? Permission.REQUEST_MOVIE
|
||||
: Permission.REQUEST_TV,
|
||||
],
|
||||
jellyseerrUser.permissions,
|
||||
{type: 'or'}
|
||||
),
|
||||
[item, jellyseerrUser]
|
||||
)
|
||||
|
||||
const canRequest = useMemo(() => {
|
||||
const status = item?.mediaInfo?.status
|
||||
return showRequestButton && !status || status === MediaStatus.UNKNOWN
|
||||
}, [item])
|
||||
const canRequest = useJellyseerrCanRequest(item);
|
||||
|
||||
return (
|
||||
<TouchableJellyseerrRouter
|
||||
@@ -62,7 +56,7 @@ const JellyseerrPoster: React.FC<Props> = ({
|
||||
<Image
|
||||
key={item.id}
|
||||
id={item.id.toString()}
|
||||
source={{uri: imageSrc}}
|
||||
source={{ uri: imageSrc }}
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
@@ -87,8 +81,7 @@ const JellyseerrPoster: React.FC<Props> = ({
|
||||
</View>
|
||||
</View>
|
||||
</TouchableJellyseerrRouter>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default JellyseerrPoster;
|
||||
export default JellyseerrPoster;
|
||||
|
||||
@@ -30,8 +30,9 @@ import { writeErrorLog } from "@/utils/log";
|
||||
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||
import {
|
||||
CombinedCredit,
|
||||
PersonDetails
|
||||
PersonDetails,
|
||||
} from "@/utils/jellyseerr/server/models/Person";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface SearchParams {
|
||||
query: string;
|
||||
@@ -220,7 +221,12 @@ export class JellyseerrApi {
|
||||
|
||||
async personCombinedCredits(id: number | string): Promise<CombinedCredit> {
|
||||
return this.axios
|
||||
?.get<CombinedCredit>(Endpoints.API_V1 + Endpoints.PERSON + `/${id}` + Endpoints.COMBINED_CREDITS)
|
||||
?.get<CombinedCredit>(
|
||||
Endpoints.API_V1 +
|
||||
Endpoints.PERSON +
|
||||
`/${id}` +
|
||||
Endpoints.COMBINED_CREDITS
|
||||
)
|
||||
.then((response) => {
|
||||
return response?.data;
|
||||
});
|
||||
@@ -260,15 +266,20 @@ export class JellyseerrApi {
|
||||
});
|
||||
}
|
||||
|
||||
imageProxy(path?: string, tmdbPath: string = 'original', width: number = 1920, quality: number = 75) {
|
||||
return path ? (
|
||||
this.axios.defaults.baseURL +
|
||||
`/_next/image?` +
|
||||
new URLSearchParams(
|
||||
`url=https://image.tmdb.org/t/p/${tmdbPath}/${path}&w=${width}&q=${quality}`
|
||||
).toString()
|
||||
) :
|
||||
this.axios?.defaults.baseURL + `/images/overseerr_poster_not_found_logo_top.png`;
|
||||
imageProxy(
|
||||
path?: string,
|
||||
tmdbPath: string = "original",
|
||||
width: number = 1920,
|
||||
quality: number = 75
|
||||
) {
|
||||
return path
|
||||
? this.axios.defaults.baseURL +
|
||||
`/_next/image?` +
|
||||
new URLSearchParams(
|
||||
`url=https://image.tmdb.org/t/p/${tmdbPath}/${path}&w=${width}&q=${quality}`
|
||||
).toString()
|
||||
: this.axios?.defaults.baseURL +
|
||||
`/images/overseerr_poster_not_found_logo_top.png`;
|
||||
}
|
||||
|
||||
async submitIssue(mediaId: number, issueType: IssueType, message: string) {
|
||||
@@ -344,6 +355,7 @@ const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER));
|
||||
export const useJellyseerr = () => {
|
||||
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const jellyseerrApi = useMemo(() => {
|
||||
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES);
|
||||
@@ -361,12 +373,16 @@ export const useJellyseerr = () => {
|
||||
|
||||
const requestMedia = useCallback(
|
||||
(title: string, request: MediaRequestBody, onSuccess?: () => void) => {
|
||||
jellyseerrApi?.request?.(request)?.then((mediaRequest) => {
|
||||
jellyseerrApi?.request?.(request)?.then(async (mediaRequest) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["search", "jellyseerr"],
|
||||
});
|
||||
|
||||
switch (mediaRequest.status) {
|
||||
case MediaRequestStatus.PENDING:
|
||||
case MediaRequestStatus.APPROVED:
|
||||
toast.success(`Requested ${title}!`);
|
||||
onSuccess?.()
|
||||
onSuccess?.();
|
||||
break;
|
||||
case MediaRequestStatus.DECLINED:
|
||||
toast.error(`You don't have permission to request!`);
|
||||
|
||||
52
utils/_jellyseerr/useJellyseerrCanRequest.ts
Normal file
52
utils/_jellyseerr/useJellyseerrCanRequest.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
} from "@/utils/jellyseerr/server/constants/media";
|
||||
import {
|
||||
hasPermission,
|
||||
Permission,
|
||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import { useMemo } from "react";
|
||||
import MediaRequest from "../jellyseerr/server/entity/MediaRequest";
|
||||
import { MovieDetails } from "../jellyseerr/server/models/Movie";
|
||||
import { TvDetails } from "../jellyseerr/server/models/Tv";
|
||||
|
||||
export const useJellyseerrCanRequest = (
|
||||
item?: MovieResult | TvResult | MovieDetails | TvDetails
|
||||
) => {
|
||||
const { jellyseerrUser } = useJellyseerr();
|
||||
|
||||
const canRequest = useMemo(() => {
|
||||
if (!jellyseerrUser || !item) return false;
|
||||
|
||||
const canNotRequest =
|
||||
item?.mediaInfo?.requests?.some(
|
||||
(r: MediaRequest) =>
|
||||
r.status == MediaRequestStatus.PENDING ||
|
||||
r.status == MediaRequestStatus.APPROVED
|
||||
) ||
|
||||
item.mediaInfo?.status === MediaStatus.AVAILABLE ||
|
||||
item.mediaInfo?.status === MediaStatus.BLACKLISTED ||
|
||||
item.mediaInfo?.status === MediaStatus.PENDING ||
|
||||
item.mediaInfo?.status === MediaStatus.PROCESSING;
|
||||
|
||||
if (canNotRequest) return false;
|
||||
|
||||
const userHasPermission = hasPermission(
|
||||
[
|
||||
Permission.REQUEST,
|
||||
item?.mediaInfo?.mediaType
|
||||
? Permission.REQUEST_MOVIE
|
||||
: Permission.REQUEST_TV,
|
||||
],
|
||||
jellyseerrUser.permissions,
|
||||
{ type: "or" }
|
||||
);
|
||||
|
||||
return userHasPermission && !canNotRequest;
|
||||
}, [item, jellyseerrUser]);
|
||||
|
||||
return canRequest;
|
||||
};
|
||||
32
utils/useReactNavigationQuery.ts
Normal file
32
utils/useReactNavigationQuery.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useFocusEffect } from "@react-navigation/core";
|
||||
import {
|
||||
QueryKey,
|
||||
useQuery,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function useReactNavigationQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = unknown,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey
|
||||
>(
|
||||
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
): UseQueryResult<TData, TError> {
|
||||
const useQueryReturn = useQuery(options);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (
|
||||
((options.refetchOnWindowFocus && useQueryReturn.isStale) ||
|
||||
options.refetchOnWindowFocus === "always") &&
|
||||
options.enabled !== false
|
||||
)
|
||||
useQueryReturn.refetch();
|
||||
}, [options.enabled, options.refetchOnWindowFocus])
|
||||
);
|
||||
|
||||
return useQueryReturn;
|
||||
}
|
||||
Reference in New Issue
Block a user