chore: Apply linting rules and add git hok (#611)

Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
This commit is contained in:
lostb1t
2025-03-16 18:01:12 +01:00
committed by GitHub
parent 2688e1b981
commit 92513e234f
268 changed files with 9197 additions and 8394 deletions

View File

@@ -1,11 +1,11 @@
import { View, ViewProps } from "react-native";
import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import React from "react";
import { FlashList } from "@shopify/flash-list";
import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { FlashList } from "@shopify/flash-list";
import type React from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
const CastSlide: React.FC<
{ details?: MovieDetails | TvDetails } & ViewProps
@@ -15,12 +15,14 @@ const CastSlide: React.FC<
details?.credits?.cast &&
details?.credits?.cast?.length > 0 && (
<View {...props}>
<Text className="text-lg font-bold mb-2 px-4">{t("jellyseerr.cast")}</Text>
<Text className='text-lg font-bold mb-2 px-4'>
{t("jellyseerr.cast")}
</Text>
<FlashList
horizontal
showsHorizontalScrollIndicator={false}
data={details?.credits.cast}
ItemSeparatorComponent={() => <View className="w-2" />}
ItemSeparatorComponent={() => <View className='w-2' />}
estimatedItemSize={15}
keyExtractor={(item) => item?.id?.toString()}
contentContainerStyle={{ paddingHorizontal: 16 }}

View File

@@ -1,15 +1,15 @@
import { View, ViewProps } from "react-native";
import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Text } from "@/components/common/Text";
import { useMemo } from "react";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { uniqBy } from "lodash";
import { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import CountryFlag from "react-native-country-flag";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { uniqBy } from "lodash";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import CountryFlag from "react-native-country-flag";
interface Release {
certification: string;
@@ -30,12 +30,12 @@ const Facts: React.FC<
> = ({ title, facts, ...props }) =>
facts &&
facts?.length > 0 && (
<View className="flex flex-col justify-between py-2" {...props}>
<Text className="font-bold text-start">{title}</Text>
<View className='flex flex-col justify-between py-2' {...props}>
<Text className='font-bold text-start'>{title}</Text>
<View className="flex flex-col items-end">
<View className='flex flex-col items-end'>
{facts.map((f, idx) =>
typeof f === "string" ? <Text key={idx}>{f}</Text> : f
typeof f === "string" ? <Text key={idx}>{f}</Text> : f,
)}
</View>
</View>
@@ -50,15 +50,19 @@ const Fact: React.FC<{ title: string; fact?: string | null } & ViewProps> = ({
const DetailFacts: React.FC<
{ details?: MovieDetails | TvDetails } & ViewProps
> = ({ details, className, ...props }) => {
const { jellyseerrUser, jellyseerrRegion: region, jellyseerrLocale: locale } = useJellyseerr();
const {
jellyseerrUser,
jellyseerrRegion: region,
jellyseerrLocale: locale,
} = useJellyseerr();
const { t } = useTranslation();
const releases = useMemo(
() =>
(details as MovieDetails)?.releases?.results.find(
(r: TmdbRelease) => r.iso_3166_1 === region
(r: TmdbRelease) => r.iso_3166_1 === region,
)?.release_dates as TmdbRelease["release_dates"],
[details]
[details],
);
// Release date types:
@@ -72,9 +76,9 @@ const DetailFacts: React.FC<
() =>
uniqBy(
releases?.filter((r: Release) => r.type > 2 && r.type < 6),
"type"
"type",
),
[releases]
[releases],
);
const firstAirDate = useMemo(() => {
@@ -82,7 +86,7 @@ const DetailFacts: React.FC<
if (firstAirDate) {
return new Date(firstAirDate).toLocaleDateString(
`${locale}-${region}`,
dateOpts
dateOpts,
);
}
}, [details]);
@@ -93,7 +97,7 @@ const DetailFacts: React.FC<
if (nextAirDate && firstAirDate !== nextAirDate) {
return new Date(nextAirDate).toLocaleDateString(
`${locale}-${region}`,
dateOpts
dateOpts,
);
}
}, [details]);
@@ -102,26 +106,26 @@ const DetailFacts: React.FC<
() =>
(details as MovieDetails)?.revenue?.toLocaleString?.(
`${locale}-${region}`,
{ style: "currency", currency: "USD" }
{ style: "currency", currency: "USD" },
),
[details]
[details],
);
const budget = useMemo(
() =>
(details as MovieDetails)?.budget?.toLocaleString?.(
`${locale}-${region}`,
{ style: "currency", currency: "USD" }
{ style: "currency", currency: "USD" },
),
[details]
[details],
);
const streamingProviders = useMemo(
() =>
details?.watchProviders?.find(
(provider) => provider.iso_3166_1 === region
(provider) => provider.iso_3166_1 === region,
)?.flatrate,
[details]
[details],
);
const networks = useMemo(() => (details as TvDetails)?.networks, [details]);
@@ -129,15 +133,15 @@ const DetailFacts: React.FC<
const spokenLanguage = useMemo(
() =>
details?.spokenLanguages.find(
(lng) => lng.iso_639_1 === details.originalLanguage
(lng) => lng.iso_639_1 === details.originalLanguage,
)?.name,
[details]
[details],
);
return (
details && (
<View className="p-4">
<Text className="text-lg font-bold">{t("jellyseerr.details")}</Text>
<View className='p-4'>
<Text className='text-lg font-bold'>{t("jellyseerr.details")}</Text>
<View
className={`${className} flex flex-col justify-center divide-y-2 divide-neutral-800`}
{...props}
@@ -148,30 +152,30 @@ const DetailFacts: React.FC<
fact={(details as TvDetails)?.originalName}
/>
{details.keywords.some(
(keyword) => keyword.id === ANIME_KEYWORD_ID
) && <Fact title={t("jellyseerr.series_type")} fact="Anime" />}
(keyword) => keyword.id === ANIME_KEYWORD_ID,
) && <Fact title={t("jellyseerr.series_type")} fact='Anime' />}
<Facts
title={t("jellyseerr.release_dates")}
facts={filteredReleases?.map?.((r: Release, idx) => (
<View key={idx} className="flex flex-row space-x-2 items-center">
<View key={idx} className='flex flex-row space-x-2 items-center'>
{r.type === 3 ? (
// Theatrical
<Ionicons name="ticket" size={16} color="white" />
<Ionicons name='ticket' size={16} color='white' />
) : r.type === 4 ? (
// Digital
<Ionicons name="cloud" size={16} color="white" />
<Ionicons name='cloud' size={16} color='white' />
) : (
// Physical
<MaterialCommunityIcons
name="record-circle-outline"
name='record-circle-outline'
size={16}
color="white"
color='white'
/>
)}
<Text>
{new Date(r.release_date).toLocaleDateString(
`${locale}-${region}`,
dateOpts
dateOpts,
)}
</Text>
</View>
@@ -181,11 +185,14 @@ const DetailFacts: React.FC<
<Fact title={t("jellyseerr.next_air_date")} fact={nextAirDate} />
<Fact title={t("jellyseerr.revenue")} fact={revenue} />
<Fact title={t("jellyseerr.budget")} fact={budget} />
<Fact title={t("jellyseerr.original_language")} fact={spokenLanguage} />
<Fact
title={t("jellyseerr.original_language")}
fact={spokenLanguage}
/>
<Facts
title={t("jellyseerr.production_country")}
facts={details?.productionCountries?.map((n, idx) => (
<View key={idx} className="flex flex-row items-center space-x-2">
<View key={idx} className='flex flex-row items-center space-x-2'>
<CountryFlag isoCode={n.iso_3166_1} size={10} />
<Text>{n.name}</Text>
</View>
@@ -194,10 +201,13 @@ const DetailFacts: React.FC<
<Facts
title={t("jellyseerr.studios")}
facts={uniqBy(details?.productionCompanies, "name")?.map(
(n) => n.name
(n) => n.name,
)}
/>
<Facts title={t("jellyseerr.network")}facts={networks?.map((n) => n.name)} />
<Facts
title={t("jellyseerr.network")}
facts={networks?.map((n) => n.name)}
/>
<Facts
title={t("jellyseerr.currently_streaming_on")}
facts={streamingProviders?.map((s) => s.name)}

View File

@@ -1,14 +1,17 @@
import Discover from "@/components/jellyseerr/discover/Discover";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import {
import type {
MovieResult,
PersonResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
import React, {useMemo, useState} from "react";
import { View, ViewProps } from "react-native";
import { orderBy, uniqBy } from "lodash";
import type React from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
import {
useAnimatedReaction,
useAnimatedStyle,
@@ -20,8 +23,6 @@ import JellyseerrPoster from "../posters/JellyseerrPoster";
import { LoadingSkeleton } from "../search/LoadingSkeleton";
import { SearchItemWrapper } from "../search/SearchItemWrapper";
import PersonPoster from "./PersonPoster";
import { useTranslation } from "react-i18next";
import {orderBy, uniqBy} from "lodash";
interface Props extends ViewProps {
searchQuery: string;
@@ -30,15 +31,15 @@ interface Props extends ViewProps {
}
export enum JellyseerrSearchSort {
DEFAULT,
VOTE_COUNT_AND_AVERAGE,
POPULARITY
DEFAULT = 0,
VOTE_COUNT_AND_AVERAGE = 1,
POPULARITY = 2,
}
export const JellyserrIndexPage: React.FC<Props> = ({
searchQuery,
sortType,
order
order,
}) => {
const { jellyseerrApi } = useJellyseerr();
const opacity = useSharedValue(1);
@@ -57,19 +58,24 @@ export const JellyserrIndexPage: React.FC<Props> = ({
const {
data: jellyseerrResults,
isFetching: f2,
isLoading: l2
isLoading: l2,
} = useReactNavigationQuery({
queryKey: ["search", "jellyseerr", "results", searchQuery],
queryFn: async () => {
const params = {
query: new URLSearchParams(searchQuery || "").toString()
}
query: new URLSearchParams(searchQuery || "").toString(),
};
return await Promise.all([
jellyseerrApi?.search({...params, page: 1}),
jellyseerrApi?.search({...params, page: 2}),
jellyseerrApi?.search({...params, page: 3}),
jellyseerrApi?.search({...params, page: 4})
]).then(all => uniqBy(all.flatMap(v => v?.results || []), "id"))
jellyseerrApi?.search({ ...params, page: 1 }),
jellyseerrApi?.search({ ...params, page: 2 }),
jellyseerrApi?.search({ ...params, page: 3 }),
jellyseerrApi?.search({ ...params, page: 4 }),
]).then((all) =>
uniqBy(
all.flatMap((v) => v?.results || []),
"id",
),
);
},
enabled: !!jellyseerrApi && searchQuery.length > 0,
});
@@ -82,57 +88,66 @@ export const JellyserrIndexPage: React.FC<Props> = ({
} else {
opacity.value = withTiming(0, { duration: 200 });
}
}
},
);
const sortingType = useMemo(
() => {
if (!sortType) return;
switch (Number(JellyseerrSearchSort[sortType])) {
case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE:
return ["voteCount", "voteAverage"];
case JellyseerrSearchSort.POPULARITY:
return ["voteCount", "popularity"]
default:
return undefined
}
},
[sortType, order]
)
const sortingType = useMemo(() => {
if (!sortType) return;
switch (Number(JellyseerrSearchSort[sortType])) {
case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE:
return ["voteCount", "voteAverage"];
case JellyseerrSearchSort.POPULARITY:
return ["voteCount", "popularity"];
default:
return undefined;
}
}, [sortType, order]);
const jellyseerrMovieResults = useMemo(
() =>
orderBy(
jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[],
sortingType || [m => m.title.toLowerCase() == searchQuery.toLowerCase()],
order || "desc"
jellyseerrResults?.filter(
(r) => r.mediaType === MediaType.MOVIE,
) as MovieResult[],
sortingType || [
(m) => m.title.toLowerCase() == searchQuery.toLowerCase(),
],
order || "desc",
),
[jellyseerrResults, sortingType, order]
[jellyseerrResults, sortingType, order],
);
const jellyseerrTvResults = useMemo(
() =>
orderBy(
jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[],
sortingType || [t => t.name.toLowerCase() == searchQuery.toLowerCase()],
order || "desc"
jellyseerrResults?.filter(
(r) => r.mediaType === MediaType.TV,
) as TvResult[],
sortingType || [
(t) => t.name.toLowerCase() == searchQuery.toLowerCase(),
],
order || "desc",
),
[jellyseerrResults, sortingType, order]
[jellyseerrResults, sortingType, order],
);
const jellyseerrPersonResults = useMemo(
() =>
orderBy(
jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[],
sortingType || [p => p.name.toLowerCase() == searchQuery.toLowerCase()],
order || "desc"
jellyseerrResults?.filter(
(r) => r.mediaType === "person",
) as PersonResult[],
sortingType || [
(p) => p.name.toLowerCase() == searchQuery.toLowerCase(),
],
order || "desc",
),
[jellyseerrResults, sortingType, order]
[jellyseerrResults, sortingType, order],
);
if (!searchQuery.length)
return (
<View className="flex flex-col">
<View className='flex flex-col'>
<Discover sliders={jellyseerrDiscoverSettings} />
</View>
);
@@ -149,10 +164,10 @@ export const JellyserrIndexPage: React.FC<Props> = ({
!l1 &&
!l2 && (
<View>
<Text className="text-center text-lg font-bold mt-4">
<Text className='text-center text-lg font-bold mt-4'>
{t("search.no_results_found_for")}
</Text>
<Text className="text-xs text-purple-600 text-center">
<Text className='text-xs text-purple-600 text-center'>
"{searchQuery}"
</Text>
</View>
@@ -178,7 +193,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
items={jellyseerrPersonResults}
renderItem={(item: PersonResult) => (
<PersonPoster
className="mr-2"
className='mr-2'
key={item.id}
id={item.id.toString()}
name={item.name}

View File

@@ -1,37 +1,32 @@
import {useMemo} from "react";
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
import {Feather, MaterialCommunityIcons} from "@expo/vector-icons";
import {View, ViewProps} from "react-native";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import { Feather, MaterialCommunityIcons } from "@expo/vector-icons";
import { useMemo } from "react";
import { View, type ViewProps } from "react-native";
const JellyseerrMediaIcon: React.FC<{ mediaType: "tv" | "movie" } & ViewProps> = ({
mediaType,
className,
...props
}) => {
const JellyseerrMediaIcon: React.FC<
{ mediaType: "tv" | "movie" } & ViewProps
> = ({ mediaType, className, ...props }) => {
const style = useMemo(
() => mediaType === MediaType.MOVIE
? 'bg-blue-600/90 border-blue-400/40'
: 'bg-purple-600/90 border-purple-400/40',
[mediaType]
() =>
mediaType === MediaType.MOVIE
? "bg-blue-600/90 border-blue-400/40"
: "bg-purple-600/90 border-purple-400/40",
[mediaType],
);
return (
mediaType &&
<View className={`${className} border ${style} rounded-full p-1`} {...props}>
{mediaType === MediaType.MOVIE ? (
<MaterialCommunityIcons
name="movie-open"
size={16}
color="white"
/>
) : (
<Feather
size={16}
name="tv"
color="white"
/>
)}
</View>
)
}
mediaType && (
<View
className={`${className} border ${style} rounded-full p-1`}
{...props}
>
{mediaType === MediaType.MOVIE ? (
<MaterialCommunityIcons name='movie-open' size={16} color='white' />
) : (
<Feather size={16} name='tv' color='white' />
)}
</View>
)
);
};
export default JellyseerrMediaIcon;
export default JellyseerrMediaIcon;

View File

@@ -1,7 +1,7 @@
import {useEffect, useState} from "react";
import {MediaStatus} from "@/utils/jellyseerr/server/constants/media";
import {MaterialCommunityIcons} from "@expo/vector-icons";
import {TouchableOpacity, View, ViewProps} from "react-native";
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native";
interface Props {
mediaStatus?: MediaStatus;
@@ -15,7 +15,8 @@ const JellyseerrStatusIcon: React.FC<Props & ViewProps> = ({
onPress,
...props
}) => {
const [badgeIcon, setBadgeIcon] = useState<keyof typeof MaterialCommunityIcons.glyphMap>();
const [badgeIcon, setBadgeIcon] =
useState<keyof typeof MaterialCommunityIcons.glyphMap>();
const [badgeStyle, setBadgeStyle] = useState<string>();
// Match similar to what Jellyseerr is currently using
@@ -23,49 +24,54 @@ const JellyseerrStatusIcon: React.FC<Props & ViewProps> = ({
useEffect(() => {
switch (mediaStatus) {
case MediaStatus.PROCESSING:
setBadgeStyle('bg-indigo-500 border-indigo-400 ring-indigo-400 text-indigo-100');
setBadgeIcon('clock');
setBadgeStyle(
"bg-indigo-500 border-indigo-400 ring-indigo-400 text-indigo-100",
);
setBadgeIcon("clock");
break;
case MediaStatus.AVAILABLE:
setBadgeStyle('bg-purple-500 border-green-400 ring-green-400 text-green-100');
setBadgeIcon('check')
setBadgeStyle(
"bg-purple-500 border-green-400 ring-green-400 text-green-100",
);
setBadgeIcon("check");
break;
case MediaStatus.PENDING:
setBadgeStyle('bg-yellow-500 border-yellow-400 ring-yellow-400 text-yellow-100');
setBadgeIcon('bell')
setBadgeStyle(
"bg-yellow-500 border-yellow-400 ring-yellow-400 text-yellow-100",
);
setBadgeIcon("bell");
break;
case MediaStatus.BLACKLISTED:
setBadgeStyle('bg-red-500 border-white-400 ring-white-400 text-white');
setBadgeIcon('eye-off')
setBadgeStyle("bg-red-500 border-white-400 ring-white-400 text-white");
setBadgeIcon("eye-off");
break;
case MediaStatus.PARTIALLY_AVAILABLE:
setBadgeStyle('bg-green-500 border-green-400 ring-green-400 text-green-100');
setBadgeStyle(
"bg-green-500 border-green-400 ring-green-400 text-green-100",
);
setBadgeIcon("minus");
break;
default:
if (showRequestIcon) {
setBadgeStyle('bg-green-600');
setBadgeIcon("plus")
setBadgeStyle("bg-green-600");
setBadgeIcon("plus");
}
break;
}
}, [mediaStatus, showRequestIcon, setBadgeStyle, setBadgeIcon])
}, [mediaStatus, showRequestIcon, setBadgeStyle, setBadgeIcon]);
return (
badgeIcon &&
<TouchableOpacity onPress={onPress} disabled={onPress == undefined}>
badgeIcon && (
<TouchableOpacity onPress={onPress} disabled={onPress == undefined}>
<View
className={`${badgeStyle ?? 'bg-purple-600'} rounded-full h-6 w-6 flex items-center justify-center ${props.className}`}
{...props}
className={`${badgeStyle ?? "bg-purple-600"} rounded-full h-6 w-6 flex items-center justify-center ${props.className}`}
{...props}
>
<MaterialCommunityIcons
name={badgeIcon}
size={18}
color="white"
/>
<MaterialCommunityIcons name={badgeIcon} size={18} color='white' />
</View>
</TouchableOpacity>
)
}
</TouchableOpacity>
)
);
};
export default JellyseerrStatusIcon;
export default JellyseerrStatusIcon;

View File

@@ -1,29 +1,27 @@
import React, {
PropsWithChildren,
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { Text } from "@/components/common/Text";
import { FlashList } from "@shopify/flash-list";
import { useFocusEffect } from "expo-router";
import type React from "react";
import {
type PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import {Dimensions, View, ViewProps} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { Text } from "@/components/common/Text";
import { Dimensions, View, type ViewProps } from "react-native";
import { Animated } from "react-native";
import { FlashList } from "@shopify/flash-list";
import {useFocusEffect} from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const ANIMATION_ENTER = 250;
const ANIMATION_EXIT = 250;
const BACKDROP_DURATION = 5000;
type Render = React.ComponentType<any>
| React.ReactElement
| null
| undefined;
type Render = React.ComponentType<any> | React.ReactElement | null | undefined;
interface Props<T> {
data: T[]
data: T[];
images: string[];
logo?: React.ReactElement;
HeaderContent?: () => React.ReactElement;
@@ -34,7 +32,7 @@ interface Props<T> {
onEndReached?: (() => void) | null | undefined;
}
const ParallaxSlideShow = <T extends unknown>({
const ParallaxSlideShow = <T,>({
data,
images,
logo,
@@ -45,8 +43,7 @@ const ParallaxSlideShow = <T extends unknown>({
keyExtractor,
onEndReached,
...props
}: PropsWithChildren<Props<T> & ViewProps>
) => {
}: PropsWithChildren<Props<T> & ViewProps>) => {
const insets = useSafeAreaInsets();
const [currentIndex, setCurrentIndex] = useState(0);
@@ -59,7 +56,7 @@ const ParallaxSlideShow = <T extends unknown>({
duration: ANIMATION_ENTER,
useNativeDriver: true,
}),
[fadeAnim]
[fadeAnim],
);
const exitAnimation = useCallback(
@@ -69,7 +66,7 @@ const ParallaxSlideShow = <T extends unknown>({
duration: ANIMATION_EXIT,
useNativeDriver: true,
}),
[fadeAnim]
[fadeAnim],
);
useEffect(() => {
@@ -77,31 +74,35 @@ const ParallaxSlideShow = <T extends unknown>({
enterAnimation().start();
const intervalId = setInterval(() => {
Animated.sequence([
enterAnimation(),
exitAnimation()
]).start(() => {
Animated.sequence([enterAnimation(), exitAnimation()]).start(() => {
fadeAnim.setValue(0);
setCurrentIndex((prevIndex) => (prevIndex + 1) % images?.length);
})
});
}, BACKDROP_DURATION);
return () => {
clearInterval(intervalId)
clearInterval(intervalId);
};
}
}, [fadeAnim, images, enterAnimation, exitAnimation, setCurrentIndex, currentIndex]);
}, [
fadeAnim,
images,
enterAnimation,
exitAnimation,
setCurrentIndex,
currentIndex,
]);
return (
<View
className="flex-1 relative"
className='flex-1 relative'
style={{
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
<ParallaxScrollView
className="flex-1 opacity-100"
className='flex-1 opacity-100'
headerHeight={300}
onEndReached={onEndReached}
headerImage={
@@ -120,9 +121,9 @@ const ParallaxSlideShow = <T extends unknown>({
}
logo={logo}
>
<View className="flex flex-col space-y-4 px-4">
<View className="flex flex-row justify-between w-full">
<View className="flex flex-col w-full">
<View className='flex flex-col space-y-4 px-4'>
<View className='flex flex-row justify-between w-full'>
<View className='flex flex-col w-full'>
{HeaderContent && HeaderContent()}
</View>
</View>
@@ -131,30 +132,30 @@ const ParallaxSlideShow = <T extends unknown>({
<FlashList
data={data}
ListEmptyComponent={
<View className="flex flex-col items-center justify-center h-full">
<Text className="font-bold text-xl text-neutral-500">
<View className='flex flex-col items-center justify-center h-full'>
<Text className='font-bold text-xl text-neutral-500'>
No results
</Text>
</View>
}
contentInsetAdjustmentBehavior="automatic"
contentInsetAdjustmentBehavior='automatic'
ListHeaderComponent={
<Text className="text-lg font-bold my-2">{listHeader}</Text>
<Text className='text-lg font-bold my-2'>{listHeader}</Text>
}
nestedScrollEnabled
showsVerticalScrollIndicator={false}
//@ts-ignore
renderItem={({ item, index}) => renderItem(item, index)}
renderItem={({ item, index }) => renderItem(item, index)}
keyExtractor={keyExtractor}
numColumns={3}
estimatedItemSize={214}
ItemSeparatorComponent={() => <View className="h-2 w-2" />}
ItemSeparatorComponent={() => <View className='h-2 w-2' />}
/>
</View>
</View>
</ParallaxScrollView>
</View>
);
}
};
export default ParallaxSlideShow;
export default ParallaxSlideShow;

View File

@@ -1,15 +1,15 @@
import {TouchableOpacity, View, ViewProps} from "react-native";
import React from "react";
import {Text} from "@/components/common/Text";
import { Text } from "@/components/common/Text";
import Poster from "@/components/posters/Poster";
import {useRouter, useSegments} from "expo-router";
import {useJellyseerr} from "@/hooks/useJellyseerr";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useRouter, useSegments } from "expo-router";
import type React from "react";
import { TouchableOpacity, View, type ViewProps } from "react-native";
interface Props {
id: string
posterPath?: string
name: string
subName?: string
id: string;
posterPath?: string;
name: string;
subName?: string;
}
const PersonPoster: React.FC<Props & ViewProps> = ({
@@ -19,24 +19,28 @@ const PersonPoster: React.FC<Props & ViewProps> = ({
subName,
...props
}) => {
const {jellyseerrApi} = useJellyseerr();
const { jellyseerrApi } = useJellyseerr();
const router = useRouter();
const segments = useSegments();
const from = segments[2];
if (from === "(home)" || from === "(search)" || from === "(libraries)")
return (
<TouchableOpacity onPress={() => router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`)}>
<View className="flex flex-col w-28" {...props}>
<TouchableOpacity
onPress={() =>
router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`)
}
>
<View className='flex flex-col w-28' {...props}>
<Poster
id={id}
url={jellyseerrApi?.imageProxy(posterPath, 'w600_and_h900_bestv2')}
url={jellyseerrApi?.imageProxy(posterPath, "w600_and_h900_bestv2")}
/>
<Text className="mt-2">{name}</Text>
{subName && <Text className="text-xs opacity-50">{subName}</Text>}
<Text className='mt-2'>{name}</Text>
{subName && <Text className='text-xs opacity-50'>{subName}</Text>}
</View>
</TouchableOpacity>
)
}
);
};
export default PersonPoster;
export default PersonPoster;

View File

@@ -1,21 +1,30 @@
import React, {forwardRef, useCallback, useMemo, useState} from "react";
import {View, ViewProps} from "react-native";
import {useJellyseerr} from "@/hooks/useJellyseerr";
import {useQuery} from "@tanstack/react-query";
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet";
import { Button } from "@/components/Button";
import Dropdown from "@/components/common/Dropdown";
import {QualityProfile, RootFolder, Tag} from "@/utils/jellyseerr/server/api/servarr/base";
import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types";
import {Button} from "@/components/Button";
import {Text} from "@/components/common/Text";
import { Text } from "@/components/common/Text";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import type {
QualityProfile,
RootFolder,
Tag,
} from "@/utils/jellyseerr/server/api/servarr/base";
import type { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
import { useQuery } from "@tanstack/react-query";
import React, { forwardRef, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native";
interface Props {
id: number;
title: string,
requestBody?: MediaRequestBody,
title: string;
requestBody?: MediaRequestBody;
type: MediaType;
isAnime?: boolean;
is4k?: boolean;
@@ -23,216 +32,252 @@ interface Props {
onDismiss?: () => void;
}
const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps, 'id'>>(({
id,
title,
requestBody,
type,
isAnime = false,
onRequested,
onDismiss,
...props
}, ref) => {
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr();
const [requestOverrides, setRequestOverrides] =
useState<MediaRequestBody>({
const RequestModal = forwardRef<
BottomSheetModalMethods,
Props & Omit<ViewProps, "id">
>(
(
{
id,
title,
requestBody,
type,
isAnime = false,
onRequested,
onDismiss,
...props
},
ref,
) => {
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
const [requestOverrides, setRequestOverrides] = useState<MediaRequestBody>({
mediaId: Number(id),
mediaType: type,
userId: jellyseerrUser?.id
userId: jellyseerrUser?.id,
});
const { t } = useTranslation();
const { t } = useTranslation();
const {data: serviceSettings} = useQuery({
queryKey: ["jellyseerr", "request", type, 'service'],
queryFn: async () => jellyseerrApi?.service(type == 'movie' ? 'radarr' : 'sonarr'),
enabled: !!jellyseerrApi && !!jellyseerrUser,
refetchOnMount: 'always'
});
const { data: serviceSettings } = useQuery({
queryKey: ["jellyseerr", "request", type, "service"],
queryFn: async () =>
jellyseerrApi?.service(type == "movie" ? "radarr" : "sonarr"),
enabled: !!jellyseerrApi && !!jellyseerrUser,
refetchOnMount: "always",
});
const {data: users} = useQuery({
queryKey: ["jellyseerr", "users"],
queryFn: async () => jellyseerrApi?.user({take: 1000, sort: 'displayname'}),
enabled: !!jellyseerrApi && !!jellyseerrUser,
refetchOnMount: 'always'
});
const { data: users } = useQuery({
queryKey: ["jellyseerr", "users"],
queryFn: async () =>
jellyseerrApi?.user({ take: 1000, sort: "displayname" }),
enabled: !!jellyseerrApi && !!jellyseerrUser,
refetchOnMount: "always",
});
const defaultService = useMemo(
() => serviceSettings?.find?.(v => v.isDefault),
[serviceSettings]
);
const defaultService = useMemo(
() => serviceSettings?.find?.((v) => v.isDefault),
[serviceSettings],
);
const {data: defaultServiceDetails} = useQuery({
queryKey: ["jellyseerr", "request", type, 'service', 'details', defaultService?.id],
queryFn: async () => {
setRequestOverrides((prev) => ({
...prev,
serverId: defaultService?.id
}))
return jellyseerrApi?.serviceDetails(type === 'movie' ? 'radarr' : 'sonarr', defaultService!!.id)
},
enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService,
refetchOnMount: 'always',
});
const { data: defaultServiceDetails } = useQuery({
queryKey: [
"jellyseerr",
"request",
type,
"service",
"details",
defaultService?.id,
],
queryFn: async () => {
setRequestOverrides((prev) => ({
...prev,
serverId: defaultService?.id,
}));
return jellyseerrApi?.serviceDetails(
type === "movie" ? "radarr" : "sonarr",
defaultService!.id,
);
},
enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService,
refetchOnMount: "always",
});
const defaultProfile: QualityProfile = useMemo(
() => defaultServiceDetails?.profiles
.find(p =>
p.id === (isAnime ? defaultServiceDetails.server?.activeAnimeProfileId : defaultServiceDetails.server?.activeProfileId)
),
[defaultServiceDetails]
);
const defaultProfile: QualityProfile = useMemo(
() =>
defaultServiceDetails?.profiles.find(
(p) =>
p.id ===
(isAnime
? defaultServiceDetails.server?.activeAnimeProfileId
: defaultServiceDetails.server?.activeProfileId),
),
[defaultServiceDetails],
);
const defaultFolder: RootFolder = useMemo(
() => defaultServiceDetails?.rootFolders
.find(f =>
f.path === (isAnime ? defaultServiceDetails?.server.activeAnimeDirectory : defaultServiceDetails.server?.activeDirectory)
),
[defaultServiceDetails]
);
const defaultFolder: RootFolder = useMemo(
() =>
defaultServiceDetails?.rootFolders.find(
(f) =>
f.path ===
(isAnime
? defaultServiceDetails?.server.activeAnimeDirectory
: defaultServiceDetails.server?.activeDirectory),
),
[defaultServiceDetails],
);
const defaultTags: Tag[] = useMemo(
() => {
const tags = defaultServiceDetails?.tags
.filter(t =>
const defaultTags: Tag[] = useMemo(() => {
const tags =
defaultServiceDetails?.tags.filter((t) =>
(isAnime
? defaultServiceDetails?.server.activeAnimeTags
: defaultServiceDetails?.server.activeTags
)?.includes(t.id)
) ?? []
return tags
},
[defaultServiceDetails]
);
)?.includes(t.id),
) ?? [];
return tags;
}, [defaultServiceDetails]);
const seasonTitle = useMemo(
() => {
const seasonTitle = useMemo(() => {
if (requestBody?.seasons && requestBody?.seasons?.length > 1) {
return t("jellyseerr.season_all")
return t("jellyseerr.season_all");
}
return t("jellyseerr.season_number", {season_number: requestBody?.seasons})
},
[requestBody?.seasons]
);
return t("jellyseerr.season_number", {
season_number: requestBody?.seasons,
});
}, [requestBody?.seasons]);
const request = useCallback(() => {requestMedia(
seasonTitle ? `${title}, ${seasonTitle}` : title,
{
is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k,
profileId: defaultProfile.id,
rootFolder: defaultFolder.path,
tags: defaultTags.map(t => t.id),
...requestBody,
...requestOverrides
},
onRequested
)
}, [requestBody, requestOverrides, defaultProfile, defaultFolder, defaultTags]);
const request = useCallback(() => {
requestMedia(
seasonTitle ? `${title}, ${seasonTitle}` : title,
{
is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k,
profileId: defaultProfile.id,
rootFolder: defaultFolder.path,
tags: defaultTags.map((t) => t.id),
...requestBody,
...requestOverrides,
},
onRequested,
);
}, [
requestBody,
requestOverrides,
defaultProfile,
defaultFolder,
defaultTags,
]);
const pathTitleExtractor = (item: RootFolder) => `${item.path} (${item.freeSpace.bytesToReadable()})`;
const pathTitleExtractor = (item: RootFolder) =>
`${item.path} (${item.freeSpace.bytesToReadable()})`;
return (
<BottomSheetModal
ref={ref}
enableDynamicSizing
enableDismissOnClose
onDismiss={onDismiss}
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
backdropComponent={(sheetProps: BottomSheetBackdropProps) =>
<BottomSheetBackdrop
{...sheetProps}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
}
>
<BottomSheetView>
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
<View>
<Text className="font-bold text-2xl text-neutral-100">{t("jellyseerr.advanced")}</Text>
{seasonTitle &&
<Text className="text-neutral-300">{seasonTitle}</Text>
}
return (
<BottomSheetModal
ref={ref}
enableDynamicSizing
enableDismissOnClose
onDismiss={onDismiss}
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
backdropComponent={(sheetProps: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...sheetProps}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
)}
>
<BottomSheetView>
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
<View>
<Text className='font-bold text-2xl text-neutral-100'>
{t("jellyseerr.advanced")}
</Text>
{seasonTitle && (
<Text className='text-neutral-300'>{seasonTitle}</Text>
)}
</View>
<View className='flex flex-col space-y-2'>
{defaultService && defaultServiceDetails && users && (
<>
<Dropdown
data={defaultServiceDetails.profiles}
titleExtractor={(item) => item.name}
placeholderText={
requestOverrides.profileName || defaultProfile.name
}
keyExtractor={(item) => item.id.toString()}
label={t("jellyseerr.quality_profile")}
onSelected={(item) =>
item &&
setRequestOverrides((prev) => ({
...prev,
profileId: item?.id,
}))
}
title={t("jellyseerr.quality_profile")}
/>
<Dropdown
data={defaultServiceDetails.rootFolders}
titleExtractor={pathTitleExtractor}
placeholderText={
defaultFolder ? pathTitleExtractor(defaultFolder) : ""
}
keyExtractor={(item) => item.id.toString()}
label={t("jellyseerr.root_folder")}
onSelected={(item) =>
item &&
setRequestOverrides((prev) => ({
...prev,
rootFolder: item.path,
}))
}
title={t("jellyseerr.root_folder")}
/>
<Dropdown
multiple
data={defaultServiceDetails.tags}
titleExtractor={(item) => item.label}
placeholderText={defaultTags.map((t) => t.label).join(",")}
keyExtractor={(item) => item.id.toString()}
label={t("jellyseerr.tags")}
onSelected={(...selected) =>
setRequestOverrides((prev) => ({
...prev,
tags: selected.map((i) => i.id),
}))
}
title={t("jellyseerr.tags")}
/>
<Dropdown
data={users}
titleExtractor={(item) => item.displayName}
placeholderText={jellyseerrUser!.displayName}
keyExtractor={(item) => item.id.toString() || ""}
label={t("jellyseerr.request_as")}
onSelected={(item) =>
item &&
setRequestOverrides((prev) => ({
...prev,
userId: item?.id,
}))
}
title={t("jellyseerr.request_as")}
/>
</>
)}
</View>
<Button className='mt-auto' onPress={request} color='purple'>
{t("jellyseerr.request_button")}
</Button>
</View>
<View className="flex flex-col space-y-2">
{(defaultService && defaultServiceDetails && users) && (
<>
<Dropdown
data={defaultServiceDetails.profiles}
titleExtractor={(item) => item.name}
placeholderText={requestOverrides.profileName || defaultProfile.name}
keyExtractor={(item) => item.id.toString()}
label={t("jellyseerr.quality_profile")}
onSelected={(item) =>
item && setRequestOverrides((prev) => ({
...prev,
profileId: item?.id
}))
}
title={t("jellyseerr.quality_profile")}
/>
<Dropdown
data={defaultServiceDetails.rootFolders}
titleExtractor={pathTitleExtractor}
placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""}
keyExtractor={(item) => item.id.toString()}
label={t("jellyseerr.root_folder")}
onSelected={(item) =>
item && setRequestOverrides((prev) => ({
...prev,
rootFolder: item.path
}))}
title={t("jellyseerr.root_folder")}
/>
<Dropdown
multiple
data={defaultServiceDetails.tags}
titleExtractor={(item) => item.label}
placeholderText={defaultTags.map(t => t.label).join(",")}
keyExtractor={(item) => item.id.toString()}
label={t("jellyseerr.tags")}
onSelected={(...selected) =>
setRequestOverrides((prev) => ({
...prev,
tags: selected.map(i => i.id)
}))
}
title={t("jellyseerr.tags")}
/>
<Dropdown
data={users}
titleExtractor={(item) => item.displayName}
placeholderText={jellyseerrUser!!.displayName}
keyExtractor={(item) => item.id.toString() || ""}
label={t("jellyseerr.request_as")}
onSelected={(item) =>
item && setRequestOverrides((prev) => ({
...prev,
userId: item?.id
}))
}
title={t("jellyseerr.request_as")}
/>
</>
)
}
</View>
<Button
className="mt-auto"
onPress={request}
color="purple"
>
{t("jellyseerr.request_button")}
</Button>
</View>
</BottomSheetView>
</BottomSheetModal>
);
});
</BottomSheetView>
</BottomSheetModal>
);
},
);
export default RequestModal;
export default RequestModal;

View File

@@ -1,14 +1,15 @@
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import {
COMPANY_LOGO_IMAGE_FILTER,
Network,
type Network,
} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
import type { Studio } from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
import { router, useSegments } from "expo-router";
import React, { useCallback } from "react";
import { TouchableOpacity, ViewProps } from "react-native";
import type React from "react";
import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
const CompanySlide: React.FC<
{ data: Network[] | Studio[] } & SlideProps & ViewProps
@@ -23,7 +24,7 @@ const CompanySlide: React.FC<
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
params: { id, image, name, type: slide.type },
}),
[slide]
[slide],
);
return (
@@ -33,13 +34,13 @@ const CompanySlide: React.FC<
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={(item, index) => (
<TouchableOpacity className="mr-2" onPress={() => navigate(item)}>
<TouchableOpacity className='mr-2' onPress={() => navigate(item)}>
<GenericSlideCard
className="w-28 rounded-lg overflow-hidden border border-neutral-900 p-4"
className='w-28 rounded-lg overflow-hidden border border-neutral-900 p-4'
id={item.id.toString()}
url={jellyseerrApi?.imageProxy(
item.image,
COMPANY_LOGO_IMAGE_FILTER
COMPANY_LOGO_IMAGE_FILTER,
)}
/>
</TouchableOpacity>

View File

@@ -1,50 +1,69 @@
import React, {useMemo} from "react";
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
import {sortBy} from "lodash";
import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide";
import CompanySlide from "@/components/jellyseerr/discover/CompanySlide";
import {View} from "react-native";
import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide";
import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { networks } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
import { studios } from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
import { sortBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import { View } from "react-native";
interface Props {
sliders?: DiscoverSlider[];
}
const Discover: React.FC<Props> = ({ sliders }) => {
if (!sliders)
return;
if (!sliders) return;
const sortedSliders = useMemo(
() => sortBy(sliders.filter((s) => s.enabled), 'order', 'asc'),
[sliders]
() =>
sortBy(
sliders.filter((s) => s.enabled),
"order",
"asc",
),
[sliders],
);
return (
<View className="flex flex-col space-y-4 mb-8">
{sortedSliders.map(slide => {
<View className='flex flex-col space-y-4 mb-8'>
{sortedSliders.map((slide) => {
switch (slide.type) {
case DiscoverSliderType.RECENT_REQUESTS:
return <RecentRequestsSlide key={slide.id} slide={slide} contentContainerStyle={{paddingBottom: 16}}/>
return (
<RecentRequestsSlide
key={slide.id}
slide={slide}
contentContainerStyle={{ paddingBottom: 16 }}
/>
);
case DiscoverSliderType.NETWORKS:
return <CompanySlide key={slide.id} slide={slide} data={networks}/>
return (
<CompanySlide key={slide.id} slide={slide} data={networks} />
);
case DiscoverSliderType.STUDIOS:
return <CompanySlide key={slide.id} slide={slide} data={studios}/>
return <CompanySlide key={slide.id} slide={slide} data={studios} />;
case DiscoverSliderType.MOVIE_GENRES:
case DiscoverSliderType.TV_GENRES:
return <GenreSlide key={slide.id} slide={slide} />
return <GenreSlide key={slide.id} slide={slide} />;
case DiscoverSliderType.TRENDING:
case DiscoverSliderType.POPULAR_MOVIES:
case DiscoverSliderType.UPCOMING_MOVIES:
case DiscoverSliderType.POPULAR_TV:
case DiscoverSliderType.UPCOMING_TV:
return <MovieTvSlide key={slide.id} slide={slide} contentContainerStyle={{paddingBottom: 16}}/>
return (
<MovieTvSlide
key={slide.id}
slide={slide}
contentContainerStyle={{ paddingBottom: 16 }}
/>
);
}
})}
</View>
)
);
};
export default Discover;

View File

@@ -1,8 +1,8 @@
import React from "react";
import {StyleSheet, View, ViewProps} from "react-native";
import {Image, ImageContentFit} from "expo-image";
import {Text} from "@/components/common/Text";
import {LinearGradient} from "expo-linear-gradient";
import { Text } from "@/components/common/Text";
import { Image, type ImageContentFit } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
import type React from "react";
import { StyleSheet, View, type ViewProps } from "react-native";
export const textShadowStyle = StyleSheet.create({
shadow: {
@@ -12,48 +12,59 @@ export const textShadowStyle = StyleSheet.create({
height: 1,
},
shadowOpacity: 1,
shadowRadius: .5,
shadowRadius: 0.5,
elevation: 6,
}
})
},
});
const GenericSlideCard: React.FC<{id: string; url?: string, title?: string, colors?: string[], contentFit?: ImageContentFit} & ViewProps> = ({
const GenericSlideCard: React.FC<
{
id: string;
url?: string;
title?: string;
colors?: string[];
contentFit?: ImageContentFit;
} & ViewProps
> = ({
id,
url,
title,
colors = ['#9333ea', 'transparent'],
colors = ["#9333ea", "transparent"],
contentFit = "contain",
...props
}) => (
<>
<LinearGradient
colors={colors}
start={{x: 0.5, y: 1.75}}
end={{x: 0.5, y: 0}}
className="rounded-xl"
start={{ x: 0.5, y: 1.75 }}
end={{ x: 0.5, y: 0 }}
className='rounded-xl'
>
<View className="rounded-xl" {...props}>
<View className='rounded-xl' {...props}>
<Image
key={id}
id={id}
source={url ? {uri: url} : null}
source={url ? { uri: url } : null}
cachePolicy={"memory-disk"}
contentFit={contentFit}
style={{
aspectRatio: "4/3",
}}
/>
{title &&
<View
className="absolute justify-center top-0 left-0 right-0 bottom-0 items-center"
{title && (
<View className='absolute justify-center top-0 left-0 right-0 bottom-0 items-center'>
<Text
className='text-center font-bold'
style={textShadowStyle.shadow}
>
<Text className="text-center font-bold" style={textShadowStyle.shadow}>{title}</Text>
</View>
}
{title}
</Text>
</View>
)}
</View>
</LinearGradient>
</>
);
export default GenericSlideCard;
export default GenericSlideCard;

View File

@@ -1,13 +1,14 @@
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
import Slide, { SlideProps } from "@/components/jellyseerr/discover/Slide";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
import { genreColorMap } from "@/utils/jellyseerr/src/components/Discover/constants";
import { useQuery } from "@tanstack/react-query";
import { router, useSegments } from "expo-router";
import React, { useCallback } from "react";
import { TouchableOpacity, ViewProps } from "react-native";
import type React from "react";
import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const segments = useSegments();
@@ -20,7 +21,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
params: { type: slide.type, name: genre.name },
}),
[slide]
[slide],
);
const { data, isFetching, isLoading } = useQuery({
@@ -29,7 +30,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
return jellyseerrApi?.getGenreSliders(
slide.type == DiscoverSliderType.MOVIE_GENRES
? Endpoints.MOVIE
: Endpoints.TV
: Endpoints.TV,
);
},
enabled: !!jellyseerrApi,
@@ -43,18 +44,18 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={(item, index) => (
<TouchableOpacity className="mr-2" onPress={() => navigate(item)}>
<TouchableOpacity className='mr-2' onPress={() => navigate(item)}>
<GenericSlideCard
className="w-28 rounded-lg overflow-hidden border border-neutral-900"
className='w-28 rounded-lg overflow-hidden border border-neutral-900'
id={item.id.toString()}
title={item.name}
colors={['transparent', 'transparent']}
colors={["transparent", "transparent"]}
contentFit={"cover"}
url={jellyseerrApi?.imageProxy(
item.backdrops?.[0],
`w780_filter(duotone,${
genreColorMap[item.id] ?? genreColorMap[0]
})`
})`,
)}
/>
</TouchableOpacity>

View File

@@ -1,18 +1,25 @@
import React, {useMemo} from "react";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import {
DiscoverEndpoint,
type DiscoverEndpoint,
Endpoints,
useJellyseerr,
} from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type {
MovieResult,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { useInfiniteQuery } from "@tanstack/react-query";
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
import {ViewProps} from "react-native";
import {uniqBy} from "lodash";
import { uniqBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import type { ViewProps } from "react-native";
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
slide,
...props
}) => {
const { jellyseerrApi } = useJellyseerr();
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
@@ -60,10 +67,12 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
const flatData = useMemo(
() =>
uniqBy(
data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
"id"
data?.pages
?.filter((p) => p?.results.length)
.flatMap((p) => p?.results),
"id",
),
[data]
[data],
);
return (
@@ -73,14 +82,16 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) =>
{...props}
slide={slide}
data={flatData}
keyExtractor={(item) => item!!.id.toString()}
keyExtractor={(item) => item!.id.toString()}
onEndReached={() => {
if (hasNextPage)
fetchNextPage()
if (hasNextPage) fetchNextPage();
}}
renderItem={(item) =>
<JellyseerrPoster item={item as MovieResult | TvResult} key={item?.id}/>
}
renderItem={(item) => (
<JellyseerrPoster
item={item as MovieResult | TvResult}
key={item?.id}
/>
)}
/>
)
);

View File

@@ -1,20 +1,28 @@
import React from "react";
import {useQuery} from "@tanstack/react-query";
import {useJellyseerr} from "@/hooks/useJellyseerr";
import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
import {ViewProps} from "react-native";
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import {NonFunctionProperties} from "@/utils/jellyseerr/server/interfaces/api/common";
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces/api/common";
import { useQuery } from "@tanstack/react-query";
import type React from "react";
import type { ViewProps } from "react-native";
const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => {
const {jellyseerrApi} = useJellyseerr();
const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => {
const { jellyseerrApi } = useJellyseerr();
const { data: details, isLoading, isError } = useQuery({
queryKey: ["jellyseerr", "detail", request.media.mediaType, request.media.tmdbId],
const {
data: details,
isLoading,
isError,
} = useQuery({
queryKey: [
"jellyseerr",
"detail",
request.media.mediaType,
request.media.tmdbId,
],
queryFn: async () => {
return request.media.mediaType == MediaType.MOVIE
? jellyseerrApi?.movieDetails(request.media.tmdbId)
: jellyseerrApi?.tvDetails(request.media.tmdbId);
@@ -34,34 +42,47 @@ const RequestCard: React.FC<{request: MediaRequest}> = ({request}) => {
});
return (
<JellyseerrPoster horizontal showDownloadInfo item={details} mediaRequest={refreshedRequest} />
)
}
<JellyseerrPoster
horizontal
showDownloadInfo
item={details}
mediaRequest={refreshedRequest}
/>
);
};
const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const {jellyseerrApi} = useJellyseerr();
const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({
slide,
...props
}) => {
const { jellyseerrApi } = useJellyseerr();
const { data: requests, isLoading, isError } = useQuery({
const {
data: requests,
isLoading,
isError,
} = useQuery({
queryKey: ["jellyseerr", "recent_requests"],
queryFn: async () => jellyseerrApi?.requests(),
queryFn: async () => jellyseerrApi?.requests(),
enabled: !!jellyseerrApi,
refetchOnMount: true,
staleTime: 0,
});
return (
requests && requests.results.length > 0 && (
requests &&
requests.results.length > 0 && (
<Slide
{...props}
slide={slide}
data={requests.results}
keyExtractor={(item) => item.id.toString()}
renderItem={(item: NonFunctionProperties<MediaRequest>) => (
<RequestCard request={item}/>
<RequestCard request={item} />
)}
/>
)
)
);
};
export default RecentRequestsSlide;
export default RecentRequestsSlide;

View File

@@ -1,11 +1,12 @@
import React, {PropsWithChildren} from "react";
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import { Text } from "@/components/common/Text";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { FlashList } from "@shopify/flash-list";
import {View, ViewProps} from "react-native";
import type { ContentStyle } from "@shopify/flash-list/src/FlashListProps";
import { t } from "i18next";
import {ContentStyle} from "@shopify/flash-list/src/FlashListProps";
import type React from "react";
import type { PropsWithChildren } from "react";
import { View, type ViewProps } from "react-native";
export interface SlideProps {
slide: DiscoverSlider;
@@ -13,17 +14,16 @@ export interface SlideProps {
}
interface Props<T> extends SlideProps {
data: T[]
renderItem: (item: T, index: number) =>
| React.ComponentType<any>
| React.ReactElement
| null
| undefined;
data: T[];
renderItem: (
item: T,
index: number,
) => React.ComponentType<any> | React.ReactElement | null | undefined;
keyExtractor: (item: T) => string;
onEndReached?: (() => void) | null | undefined;
}
const Slide = <T extends unknown>({
const Slide = <T,>({
data,
slide,
renderItem,
@@ -31,18 +31,17 @@ const Slide = <T extends unknown>({
onEndReached,
contentContainerStyle,
...props
}: PropsWithChildren<Props<T> & ViewProps>
) => {
}: PropsWithChildren<Props<T> & ViewProps>) => {
return (
<View {...props}>
<Text className="font-bold text-lg mb-2 px-4">
<Text className='font-bold text-lg mb-2 px-4'>
{t("search." + DiscoverSliderType[slide.type].toString().toLowerCase())}
</Text>
<FlashList
horizontal
contentContainerStyle={{
paddingHorizontal: 16,
...(contentContainerStyle ? contentContainerStyle : {})
...(contentContainerStyle ? contentContainerStyle : {}),
}}
showsHorizontalScrollIndicator={false}
keyExtractor={keyExtractor}
@@ -51,7 +50,9 @@ const Slide = <T extends unknown>({
onEndReachedThreshold={1}
onEndReached={onEndReached}
//@ts-ignore
renderItem={({item, index}) => item ? renderItem(item, index) : <></>}
renderItem={({ item, index }) =>
item ? renderItem(item, index) : <></>
}
/>
</View>
);