refactor: rename Jellyseerr to Seerr throughout codebase

Updates branding and naming conventions to use "Seerr" instead of "Jellyseerr" across all files, components, hooks, and translations.

Renames files, functions, classes, variables, and UI text to reflect the new naming convention while maintaining identical functionality. Updates asset references including logo and screenshot images.

Changes API class name, storage keys, atom names, and all related utilities to use "Seerr" prefix. Modifies translation keys and user-facing text to match the rebrand.
This commit is contained in:
Uruk
2026-01-12 09:26:19 +01:00
committed by Gauvain
parent b7db06f53d
commit f211a9ce7a
47 changed files with 599 additions and 680 deletions

View File

@@ -3,7 +3,7 @@
## Project Overview ## Project Overview
Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native). Streamyfin is a cross-platform Jellyfin video streaming client built with Expo (React Native).
It supports mobile (iOS/Android) and TV platforms, integrates with Jellyfin and Jellyseerr APIs, It supports mobile (iOS/Android) and TV platforms, integrates with Jellyfin and Seerr APIs,
and provides seamless media streaming with offline capabilities and Chromecast support. and provides seamless media streaming with offline capabilities and Chromecast support.
## Main Technologies ## Main Technologies

View File

@@ -16,7 +16,7 @@
   
<img src="./assets/images/screenshots/screenshot2.png" width="20%"> <img src="./assets/images/screenshots/screenshot2.png" width="20%">
&nbsp; &nbsp;
<img src="./assets/images/jellyseerr.PNG" width="21%"> <img src="./assets/images/seerr.PNG" width="21%">
</p> </p>

View File

@@ -222,9 +222,9 @@ export default function IndexLayout() {
}} }}
/> />
<Stack.Screen <Stack.Screen
name='settings/plugins/jellyseerr/page' name='settings/plugins/seerr/page'
options={{ options={{
title: "Jellyseerr", title: "Seerr",
headerBlurEffect: "none", headerBlurEffect: "none",
headerTransparent: Platform.OS === "ios", headerTransparent: Platform.OS === "ios",
headerShadowVisible: false, headerShadowVisible: false,

View File

@@ -1,10 +1,10 @@
import { ScrollView } from "react-native"; import { ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import DisabledSetting from "@/components/settings/DisabledSetting"; import DisabledSetting from "@/components/settings/DisabledSetting";
import { JellyseerrSettings } from "@/components/settings/Jellyseerr"; import { SeerrSettings } from "@/components/settings/Seerr";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
export default function page() { export default function Page() {
const { pluginSettings } = useSettings(); const { pluginSettings } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -17,10 +17,10 @@ export default function page() {
}} }}
> >
<DisabledSetting <DisabledSetting
disabled={pluginSettings?.jellyseerrServerUrl?.locked === true} disabled={pluginSettings?.seerrServerUrl?.locked === true}
className='px-4' className='px-4'
> >
<JellyseerrSettings /> <SeerrSettings />
</DisabledSetting> </DisabledSetting>
</ScrollView> </ScrollView>
); );

View File

@@ -3,9 +3,9 @@ import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { uniqBy } from "lodash"; import { uniqBy } from "lodash";
import { useMemo } from "react"; import { useMemo } from "react";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import SeerrPoster from "@/components/posters/SeerrPoster";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import ParallaxSlideShow from "@/components/seerr/ParallaxSlideShow";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { Endpoints, useSeerr } from "@/hooks/useSeerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import { import {
type MovieResult, type MovieResult,
@@ -13,9 +13,9 @@ import {
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
export default function page() { export default function CompanyPage() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); const { seerrApi, isSeerrMovieOrTvResult } = useSeerr();
const { companyId, image, type } = local as unknown as { const { companyId, image, type } = local as unknown as {
companyId: string; companyId: string;
@@ -25,12 +25,12 @@ export default function page() {
}; };
const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery({ const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery({
queryKey: ["jellyseerr", "company", type, companyId], queryKey: ["seerr", "company", type, companyId],
queryFn: async ({ pageParam }) => { queryFn: async ({ pageParam }) => {
const params: any = { const params: any = {
page: Number(pageParam), page: Number(pageParam),
}; };
return jellyseerrApi?.discover( return seerrApi?.discover(
`${ `${
Number(type) === DiscoverSliderType.NETWORKS Number(type) === DiscoverSliderType.NETWORKS
? Endpoints.DISCOVER_TV_NETWORK ? Endpoints.DISCOVER_TV_NETWORK
@@ -39,7 +39,7 @@ export default function page() {
params, params,
); );
}, },
enabled: !!jellyseerrApi && !!companyId, enabled: !!seerrApi && !!companyId,
initialPageParam: 1, initialPageParam: 1,
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
(lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) + (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
@@ -53,8 +53,7 @@ export default function page() {
data?.pages data?.pages
?.filter((p) => p?.results.length) ?.filter((p) => p?.results.length)
.flatMap( .flatMap(
(p) => (p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r)) ?? [],
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [],
), ),
"id", "id",
) ?? [], ) ?? [],
@@ -63,15 +62,15 @@ export default function page() {
const backdrops = useMemo( const backdrops = useMemo(
() => () =>
jellyseerrApi seerrApi
? flatData.map((r) => ? flatData.map((r) =>
jellyseerrApi.imageProxy( seerrApi.imageProxy(
(r as TvResult | MovieResult).backdropPath, (r as TvResult | MovieResult).backdropPath,
"w1920_and_h800_multi_faces", "w1920_and_h800_multi_faces",
), ),
) )
: [], : [],
[jellyseerrApi, flatData], [seerrApi, flatData],
); );
return ( return (
@@ -92,7 +91,7 @@ export default function page() {
key={companyId} key={companyId}
className='bottom-1 w-1/2' className='bottom-1 w-1/2'
source={{ source={{
uri: jellyseerrApi?.imageProxy(image, COMPANY_LOGO_IMAGE_FILTER), uri: seerrApi?.imageProxy(image, COMPANY_LOGO_IMAGE_FILTER),
}} }}
cachePolicy={"memory-disk"} cachePolicy={"memory-disk"}
contentFit='contain' contentFit='contain'
@@ -101,7 +100,7 @@ export default function page() {
}} }}
/> />
} }
renderItem={(item, _index) => <JellyseerrPoster item={item} />} renderItem={(item, _index) => <SeerrPoster item={item} />}
/> />
); );
} }

View File

@@ -3,15 +3,15 @@ import { useLocalSearchParams } from "expo-router";
import { uniqBy } from "lodash"; import { uniqBy } from "lodash";
import { useMemo } from "react"; import { useMemo } from "react";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; import SeerrPoster from "@/components/posters/SeerrPoster";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import { textShadowStyle } from "@/components/seerr/discover/GenericSlideCard";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import ParallaxSlideShow from "@/components/seerr/ParallaxSlideShow";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { Endpoints, useSeerr } from "@/hooks/useSeerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
export default function page() { export default function GenrePage() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); const { seerrApi, isSeerrMovieOrTvResult } = useSeerr();
const { genreId, name, type } = local as unknown as { const { genreId, name, type } = local as unknown as {
genreId: string; genreId: string;
@@ -20,21 +20,21 @@ export default function page() {
}; };
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "company", type, genreId], queryKey: ["seerr", "company", type, genreId],
queryFn: async ({ pageParam }) => { queryFn: async ({ pageParam }) => {
const params: any = { const params: any = {
page: Number(pageParam), page: Number(pageParam),
genre: genreId, genre: genreId,
}; };
return jellyseerrApi?.discover( return seerrApi?.discover(
type === DiscoverSliderType.MOVIE_GENRES type === DiscoverSliderType.MOVIE_GENRES
? Endpoints.DISCOVER_MOVIES ? Endpoints.DISCOVER_MOVIES
: Endpoints.DISCOVER_TV, : Endpoints.DISCOVER_TV,
params, params,
); );
}, },
enabled: !!jellyseerrApi && !!genreId, enabled: !!seerrApi && !!genreId,
initialPageParam: 1, initialPageParam: 1,
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
(lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) + (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
@@ -48,8 +48,7 @@ export default function page() {
data?.pages data?.pages
?.filter((p) => p?.results.length) ?.filter((p) => p?.results.length)
.flatMap( .flatMap(
(p) => (p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r)) ?? [],
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [],
), ),
"id", "id",
) ?? [], ) ?? [],
@@ -58,15 +57,12 @@ export default function page() {
const backdrops = useMemo( const backdrops = useMemo(
() => () =>
jellyseerrApi seerrApi
? flatData.map((r) => ? flatData.map((r) =>
jellyseerrApi.imageProxy( seerrApi.imageProxy(r.backdropPath, "w1920_and_h800_multi_faces"),
r.backdropPath,
"w1920_and_h800_multi_faces",
),
) )
: [], : [],
[jellyseerrApi, flatData], [seerrApi, flatData],
); );
return ( return (
@@ -91,7 +87,7 @@ export default function page() {
{name} {name}
</Text> </Text>
} }
renderItem={(item, _index) => <JellyseerrPoster item={item} />} renderItem={(item, _index) => <SeerrPoster item={item} />}
/> />
); );
} }

View File

@@ -18,18 +18,18 @@ import { toast } from "sonner-native";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { GenreTags } from "@/components/GenreTags"; import { GenreTags } from "@/components/GenreTags";
import Cast from "@/components/jellyseerr/Cast";
import DetailFacts from "@/components/jellyseerr/DetailFacts";
import RequestModal from "@/components/jellyseerr/RequestModal";
import { OverviewText } from "@/components/OverviewText"; import { OverviewText } from "@/components/OverviewText";
import { ParallaxScrollView } from "@/components/ParallaxPage"; import { ParallaxScrollView } from "@/components/ParallaxPage";
import { PlatformDropdown } from "@/components/PlatformDropdown"; import { PlatformDropdown } from "@/components/PlatformDropdown";
import { JellyserrRatings } from "@/components/Ratings"; import { SeerrRatings } from "@/components/Ratings";
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; import Cast from "@/components/seerr/Cast";
import DetailFacts from "@/components/seerr/DetailFacts";
import RequestModal from "@/components/seerr/RequestModal";
import SeerrSeasons from "@/components/series/SeerrSeasons";
import { ItemActions } from "@/components/series/SeriesActions"; import { ItemActions } from "@/components/series/SeriesActions";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; import { useSeerrCanRequest } from "@/utils/_seerr/useSeerrCanRequest";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import { import {
type IssueType, type IssueType,
@@ -68,7 +68,7 @@ const Page: React.FC = () => {
} & Partial<MovieResult | TvResult | MovieDetails | TvDetails>; } & Partial<MovieResult | TvResult | MovieDetails | TvDetails>;
const navigation = useNavigation(); const navigation = useNavigation();
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); const { seerrApi, seerrUser, requestMedia } = useSeerr();
const [issueType, setIssueType] = useState<IssueType>(); const [issueType, setIssueType] = useState<IssueType>();
const [issueMessage, setIssueMessage] = useState<string>(); const [issueMessage, setIssueMessage] = useState<string>();
@@ -83,8 +83,8 @@ const Page: React.FC = () => {
isLoading, isLoading,
refetch, refetch,
} = useQuery({ } = useQuery({
enabled: !!jellyseerrApi && !!result && !!result.id, enabled: !!seerrApi && !!result && !!result.id,
queryKey: ["jellyseerr", "detail", mediaType, result.id], queryKey: ["seerr", "detail", mediaType, result.id],
staleTime: 0, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
refetchOnReconnect: true, refetchOnReconnect: true,
@@ -93,21 +93,18 @@ const Page: React.FC = () => {
refetchInterval: 0, refetchInterval: 0,
queryFn: async () => { queryFn: async () => {
return mediaType === MediaType.MOVIE return mediaType === MediaType.MOVIE
? jellyseerrApi?.movieDetails(result.id!) ? seerrApi?.movieDetails(result.id!)
: jellyseerrApi?.tvDetails(result.id!); : seerrApi?.tvDetails(result.id!);
}, },
}); });
const [canRequest, hasAdvancedRequestPermission] = const [canRequest, hasAdvancedRequestPermission] =
useJellyseerrCanRequest(details); useSeerrCanRequest(details);
const canManageRequests = useMemo(() => { const canManageRequests = useMemo(() => {
if (!jellyseerrUser) return false; if (!seerrUser) return false;
return hasPermission( return hasPermission(Permission.MANAGE_REQUESTS, seerrUser.permissions);
Permission.MANAGE_REQUESTS, }, [seerrUser]);
jellyseerrUser.permissions,
);
}, [jellyseerrUser]);
const pendingRequest = useMemo(() => { const pendingRequest = useMemo(() => {
return details?.mediaInfo?.requests?.find( return details?.mediaInfo?.requests?.find(
@@ -119,27 +116,27 @@ const Page: React.FC = () => {
if (!pendingRequest?.id) return; if (!pendingRequest?.id) return;
try { try {
await jellyseerrApi?.approveRequest(pendingRequest.id); await seerrApi?.approveRequest(pendingRequest.id);
toast.success(t("jellyseerr.toasts.request_approved")); toast.success(t("seerr.toasts.request_approved"));
refetch(); refetch();
} catch (error) { } catch (error) {
toast.error(t("jellyseerr.toasts.failed_to_approve_request")); toast.error(t("seerr.toasts.failed_to_approve_request"));
console.error("Failed to approve request:", error); console.error("Failed to approve request:", error);
} }
}, [jellyseerrApi, pendingRequest, refetch, t]); }, [seerrApi, pendingRequest, refetch, t]);
const handleDeclineRequest = useCallback(async () => { const handleDeclineRequest = useCallback(async () => {
if (!pendingRequest?.id) return; if (!pendingRequest?.id) return;
try { try {
await jellyseerrApi?.declineRequest(pendingRequest.id); await seerrApi?.declineRequest(pendingRequest.id);
toast.success(t("jellyseerr.toasts.request_declined")); toast.success(t("seerr.toasts.request_declined"));
refetch(); refetch();
} catch (error) { } catch (error) {
toast.error(t("jellyseerr.toasts.failed_to_decline_request")); toast.error(t("seerr.toasts.failed_to_decline_request"));
console.error("Failed to decline request:", error); console.error("Failed to decline request:", error);
} }
}, [jellyseerrApi, pendingRequest, refetch, t]); }, [seerrApi, pendingRequest, refetch, t]);
const renderBackdrop = useCallback( const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => ( (props: BottomSheetBackdropProps) => (
@@ -154,7 +151,7 @@ const Page: React.FC = () => {
const submitIssue = useCallback(() => { const submitIssue = useCallback(() => {
if (result.id && issueType && issueMessage && details) { if (result.id && issueType && issueMessage && details) {
jellyseerrApi seerrApi
?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage) ?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage)
.then(() => { .then(() => {
setIssueType(undefined); setIssueType(undefined);
@@ -162,7 +159,7 @@ const Page: React.FC = () => {
bottomSheetModalRef?.current?.close(); bottomSheetModalRef?.current?.close();
}); });
} }
}, [jellyseerrApi, details, result, issueType, issueMessage]); }, [seerrApi, details, result, issueType, issueMessage]);
const handleIssueModalDismiss = useCallback(() => { const handleIssueModalDismiss = useCallback(() => {
setIssueTypeDropdownOpen(false); setIssueTypeDropdownOpen(false);
@@ -214,7 +211,7 @@ const Page: React.FC = () => {
const issueTypeOptionGroups = useMemo( const issueTypeOptionGroups = useMemo(
() => [ () => [
{ {
title: t("jellyseerr.types"), title: t("seerr.types"),
options: Object.entries(IssueTypeName) options: Object.entries(IssueTypeName)
.reverse() .reverse()
.map(([key, value]) => ({ .map(([key, value]) => ({
@@ -265,7 +262,7 @@ const Page: React.FC = () => {
height: "100%", height: "100%",
}} }}
source={{ source={{
uri: jellyseerrApi?.imageProxy( uri: seerrApi?.imageProxy(
result.backdropPath, result.backdropPath,
"w1920_and_h800_multi_faces", "w1920_and_h800_multi_faces",
), ),
@@ -295,7 +292,7 @@ const Page: React.FC = () => {
<View className='px-4'> <View className='px-4'>
<View className='flex flex-row justify-between w-full'> <View className='flex flex-row justify-between w-full'>
<View className='flex flex-col w-56'> <View className='flex flex-col w-56'>
<JellyserrRatings <SeerrRatings
result={ result={
result as result as
| MovieResult | MovieResult
@@ -330,7 +327,7 @@ const Page: React.FC = () => {
/> />
) : canRequest ? ( ) : canRequest ? (
<Button color='purple' onPress={request} className='mt-4'> <Button color='purple' onPress={request} className='mt-4'>
{t("jellyseerr.request_button")} {t("seerr.request_button")}
</Button> </Button>
) : ( ) : (
details?.mediaInfo?.jellyfinMediaId && ( details?.mediaInfo?.jellyfinMediaId && (
@@ -353,7 +350,7 @@ const Page: React.FC = () => {
}} }}
> >
<Text className='text-sm'> <Text className='text-sm'>
{t("jellyseerr.report_issue_button")} {t("seerr.report_issue_button")}
</Text> </Text>
</Button> </Button>
)} )}
@@ -389,12 +386,12 @@ const Page: React.FC = () => {
<View className='flex flex-row items-center space-x-2'> <View className='flex flex-row items-center space-x-2'>
<Ionicons name='person-outline' size={16} color='#9CA3AF' /> <Ionicons name='person-outline' size={16} color='#9CA3AF' />
<Text className='text-sm text-neutral-400'> <Text className='text-sm text-neutral-400'>
{t("jellyseerr.requested_by", { {t("seerr.requested_by", {
user: user:
pendingRequest.requestedBy?.displayName || pendingRequest.requestedBy?.displayName ||
pendingRequest.requestedBy?.username || pendingRequest.requestedBy?.username ||
pendingRequest.requestedBy?.jellyfinUsername || pendingRequest.requestedBy?.jellyfinUsername ||
t("jellyseerr.unknown_user"), t("seerr.unknown_user"),
})} })}
</Text> </Text>
</View> </View>
@@ -415,7 +412,7 @@ const Page: React.FC = () => {
borderStyle: "solid", borderStyle: "solid",
}} }}
> >
<Text className='text-sm'>{t("jellyseerr.approve")}</Text> <Text className='text-sm'>{t("seerr.approve")}</Text>
</Button> </Button>
<Button <Button
className='flex-1 bg-red-600/50 border-red-400 ring-red-400 text-red-100' className='flex-1 bg-red-600/50 border-red-400 ring-red-400 text-red-100'
@@ -433,7 +430,7 @@ const Page: React.FC = () => {
borderStyle: "solid", borderStyle: "solid",
}} }}
> >
<Text className='text-sm'>{t("jellyseerr.decline")}</Text> <Text className='text-sm'>{t("seerr.decline")}</Text>
</Button> </Button>
</View> </View>
</View> </View>
@@ -442,7 +439,7 @@ const Page: React.FC = () => {
</View> </View>
{mediaType === MediaType.TV && ( {mediaType === MediaType.TV && (
<JellyseerrSeasons <SeerrSeasons
isLoading={isLoading || isFetching} isLoading={isLoading || isFetching}
details={details as TvDetails} details={details as TvDetails}
refetch={refetch} refetch={refetch}
@@ -491,13 +488,13 @@ const Page: React.FC = () => {
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
<View> <View>
<Text className='font-bold text-2xl text-neutral-100'> <Text className='font-bold text-2xl text-neutral-100'>
{t("jellyseerr.whats_wrong")} {t("seerr.whats_wrong")}
</Text> </Text>
</View> </View>
<View className='flex flex-col space-y-2 items-start'> <View className='flex flex-col space-y-2 items-start'>
<View className='flex flex-col w-full'> <View className='flex flex-col w-full'>
<Text className='opacity-50 mb-1 text-xs'> <Text className='opacity-50 mb-1 text-xs'>
{t("jellyseerr.issue_type")} {t("seerr.issue_type")}
</Text> </Text>
<PlatformDropdown <PlatformDropdown
groups={issueTypeOptionGroups} groups={issueTypeOptionGroups}
@@ -506,11 +503,11 @@ const Page: React.FC = () => {
<Text numberOfLines={1}> <Text numberOfLines={1}>
{issueType {issueType
? IssueTypeName[issueType] ? IssueTypeName[issueType]
: t("jellyseerr.select_an_issue")} : t("seerr.select_an_issue")}
</Text> </Text>
</View> </View>
} }
title={t("jellyseerr.types")} title={t("seerr.types")}
open={issueTypeDropdownOpen} open={issueTypeDropdownOpen}
onOpenChange={setIssueTypeDropdownOpen} onOpenChange={setIssueTypeDropdownOpen}
/> />
@@ -522,7 +519,7 @@ const Page: React.FC = () => {
maxLength={254} maxLength={254}
style={{ color: "white" }} style={{ color: "white" }}
clearButtonMode='always' clearButtonMode='always'
placeholder={t("jellyseerr.describe_the_issue")} placeholder={t("seerr.describe_the_issue")}
placeholderTextColor='#9CA3AF' placeholderTextColor='#9CA3AF'
// Issue with multiline + Textinput inside a portal // Issue with multiline + Textinput inside a portal
// https://github.com/callstack/react-native-paper/issues/1668 // https://github.com/callstack/react-native-paper/issues/1668
@@ -532,7 +529,7 @@ const Page: React.FC = () => {
</View> </View>
</View> </View>
<Button className='mt-auto' onPress={submitIssue} color='purple'> <Button className='mt-auto' onPress={submitIssue} color='purple'>
{t("jellyseerr.submit_button")} {t("seerr.submit_button")}
</Button> </Button>
</View> </View>
</BottomSheetView> </BottomSheetView>

View File

@@ -5,31 +5,27 @@ import { orderBy, uniqBy } from "lodash";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import { OverviewText } from "@/components/OverviewText"; import { OverviewText } from "@/components/OverviewText";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import SeerrPoster from "@/components/posters/SeerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import ParallaxSlideShow from "@/components/seerr/ParallaxSlideShow";
import { useSeerr } from "@/hooks/useSeerr";
import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
export default function page() { export default function PersonPage() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
const { t } = useTranslation(); const { t } = useTranslation();
const { const { seerrApi, seerrRegion: region, seerrLocale: locale } = useSeerr();
jellyseerrApi,
jellyseerrRegion: region,
jellyseerrLocale: locale,
} = useJellyseerr();
const { personId } = local as { personId: string }; const { personId } = local as { personId: string };
const { data } = useQuery({ const { data } = useQuery({
queryKey: ["jellyseerr", "person", personId], queryKey: ["seerr", "person", personId],
queryFn: async () => ({ queryFn: async () => ({
details: await jellyseerrApi?.personDetails(personId), details: await seerrApi?.personDetails(personId),
combinedCredits: await jellyseerrApi?.personCombinedCredits(personId), combinedCredits: await seerrApi?.personCombinedCredits(personId),
}), }),
enabled: !!jellyseerrApi && !!personId, enabled: !!seerrApi && !!personId,
}); });
const castedRoles: PersonCreditCast[] = useMemo( const castedRoles: PersonCreditCast[] = useMemo(
@@ -46,22 +42,19 @@ export default function page() {
); );
const backdrops = useMemo( const backdrops = useMemo(
() => () =>
jellyseerrApi seerrApi
? castedRoles.map((c) => ? castedRoles.map((c) =>
jellyseerrApi.imageProxy( seerrApi.imageProxy(c.backdropPath, "w1920_and_h800_multi_faces"),
c.backdropPath,
"w1920_and_h800_multi_faces",
),
) )
: [], : [],
[jellyseerrApi, data?.combinedCredits], [seerrApi, data?.combinedCredits],
); );
return ( return (
<ParallaxSlideShow <ParallaxSlideShow
data={castedRoles} data={castedRoles}
images={backdrops} images={backdrops}
listHeader={t("jellyseerr.appearances")} listHeader={t("seerr.appearances")}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
logo={ logo={
<Image <Image
@@ -69,7 +62,7 @@ export default function page() {
id={data?.details?.id.toString()} id={data?.details?.id.toString()}
className='rounded-full bottom-1' className='rounded-full bottom-1'
source={{ source={{
uri: jellyseerrApi?.imageProxy( uri: seerrApi?.imageProxy(
data?.details?.profilePath, data?.details?.profilePath,
"w600_and_h600_bestv2", "w600_and_h600_bestv2",
), ),
@@ -86,7 +79,7 @@ export default function page() {
<> <>
<Text className='font-bold text-2xl mb-1'>{data?.details?.name}</Text> <Text className='font-bold text-2xl mb-1'>{data?.details?.name}</Text>
<Text className='opacity-50'> <Text className='opacity-50'>
{t("jellyseerr.born")}{" "} {t("seerr.born")}{" "}
{data?.details?.birthday && {data?.details?.birthday &&
new Date(data.details.birthday).toLocaleDateString( new Date(data.details.birthday).toLocaleDateString(
`${locale}-${region}`, `${locale}-${region}`,
@@ -103,7 +96,7 @@ export default function page() {
MainContent={() => ( MainContent={() => (
<OverviewText text={data?.details?.biography} className='mt-4' /> <OverviewText text={data?.details?.biography} className='mt-4' />
)} )}
renderItem={(item, _index) => <JellyseerrPoster item={item} />} renderItem={(item, _index) => <SeerrPoster item={item} />}
/> />
); );
} }

View File

@@ -33,17 +33,17 @@ export default function SearchLayout() {
headerShadowVisible: false, headerShadowVisible: false,
}} }}
/> />
<Stack.Screen name='jellyseerr/page' options={commonScreenOptions} /> <Stack.Screen name='seerr/page' options={commonScreenOptions} />
<Stack.Screen <Stack.Screen
name='jellyseerr/person/[personId]' name='seerr/person/[personId]'
options={commonScreenOptions} options={commonScreenOptions}
/> />
<Stack.Screen <Stack.Screen
name='jellyseerr/company/[companyId]' name='seerr/company/[companyId]'
options={commonScreenOptions} options={commonScreenOptions}
/> />
<Stack.Screen <Stack.Screen
name='jellyseerr/genre/[genreId]' name='seerr/genre/[genreId]'
options={commonScreenOptions} options={commonScreenOptions}
/> />
</Stack> </Stack>

View File

@@ -26,18 +26,18 @@ import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText"; import { ItemCardText } from "@/components/ItemCardText";
import {
JellyseerrSearchSort,
JellyserrIndexPage,
} from "@/components/jellyseerr/JellyseerrIndexPage";
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 { DiscoverFilters } from "@/components/search/DiscoverFilters"; import { DiscoverFilters } from "@/components/search/DiscoverFilters";
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
import { SearchTabButtons } from "@/components/search/SearchTabButtons"; import { SearchTabButtons } from "@/components/search/SearchTabButtons";
import {
SeerrIndexPage,
SeerrSearchSort,
} from "@/components/seerr/SeerrIndexPage";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
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 { eventBus } from "@/utils/eventBus"; import { eventBus } from "@/utils/eventBus";
@@ -55,7 +55,7 @@ const exampleSearches = [
"The Mandalorian", "The Mandalorian",
]; ];
export default function search() { export default function SearchPage() {
const params = useLocalSearchParams(); const params = useLocalSearchParams();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter(); const router = useRouter();
@@ -93,16 +93,11 @@ export default function search() {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const { settings } = useSettings(); const { settings } = useSettings();
const { jellyseerrApi } = useJellyseerr(); const { seerrApi } = useSeerr();
const [jellyseerrOrderBy, setJellyseerrOrderBy] = const [seerrOrderBy, setSeerrOrderBy] = useState<SeerrSearchSort>(
useState<JellyseerrSearchSort>( SeerrSearchSort[SeerrSearchSort.DEFAULT] as unknown as SeerrSearchSort,
JellyseerrSearchSort[ );
JellyseerrSearchSort.DEFAULT const [seerrSortOrder, setSeerrSortOrder] = useState<"asc" | "desc">("desc");
] as unknown as JellyseerrSearchSort,
);
const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<
"asc" | "desc"
>("desc");
const searchEngine = useMemo(() => { const searchEngine = useMemo(() => {
return settings?.searchEngine || "Jellyfin"; return settings?.searchEngine || "Jellyfin";
@@ -474,7 +469,7 @@ export default function search() {
className='flex flex-col' className='flex flex-col'
style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }} style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }}
> >
{jellyseerrApi && ( {seerrApi && (
<View className='pl-4 pr-4 flex flex-row'> <View className='pl-4 pr-4 flex flex-row'>
<SearchTabButtons <SearchTabButtons
searchType={searchType} searchType={searchType}
@@ -488,10 +483,10 @@ export default function search() {
<DiscoverFilters <DiscoverFilters
searchFilterId={searchFilterId} searchFilterId={searchFilterId}
orderFilterId={orderFilterId} orderFilterId={orderFilterId}
jellyseerrOrderBy={jellyseerrOrderBy} seerrOrderBy={seerrOrderBy}
setJellyseerrOrderBy={setJellyseerrOrderBy} setSeerrOrderBy={setSeerrOrderBy}
jellyseerrSortOrder={jellyseerrSortOrder} seerrSortOrder={seerrSortOrder}
setJellyseerrSortOrder={setJellyseerrSortOrder} setSeerrSortOrder={setSeerrSortOrder}
t={t} t={t}
/> />
)} )}
@@ -754,10 +749,10 @@ export default function search() {
/> />
</View> </View>
) : ( ) : (
<JellyserrIndexPage <SeerrIndexPage
searchQuery={debouncedSearch} searchQuery={debouncedSearch}
sortType={jellyseerrOrderBy} sortType={seerrOrderBy}
order={jellyseerrSortOrder} order={seerrSortOrder}
/> />
)} )}

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -89,16 +89,16 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
</Text> </Text>
<View className='flex flex-row items-center mt-4'> <View className='flex flex-row items-center mt-4'>
<Image <Image
source={require("@/assets/icons/jellyseerr-logo.svg")} source={require("@/assets/icons/seerr-logo.svg")}
style={{ style={{
width: 50, width: 50,
height: 50, height: 50,
}} }}
/> />
<View className='shrink ml-2'> <View className='shrink ml-2'>
<Text className='font-bold mb-1'>Jellyseerr</Text> <Text className='font-bold mb-1'>Seerr</Text>
<Text className='shrink text-xs'> <Text className='shrink text-xs'>
{t("home.intro.jellyseerr_feature_description")} {t("home.intro.seerr_feature_description")}
</Text> </Text>
</View> </View>
</View> </View>

View File

@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useMemo } from "react"; import { useMemo } from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { import type {
@@ -55,23 +55,23 @@ export const Ratings: React.FC<Props> = ({ item, ...props }) => {
); );
}; };
export const JellyserrRatings: React.FC<{ export const SeerrRatings: React.FC<{
result: MovieResult | TvResult | TvDetails | MovieDetails; result: MovieResult | TvResult | TvDetails | MovieDetails;
}> = ({ result }) => { }> = ({ result }) => {
const { jellyseerrApi, getMediaType } = useJellyseerr(); const { seerrApi, getMediaType } = useSeerr();
const mediaType = useMemo(() => getMediaType(result), [result]); const mediaType = useMemo(() => getMediaType(result), [result]);
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["jellyseerr", result.id, mediaType, "ratings"], queryKey: ["seerr", result.id, mediaType, "ratings"],
queryFn: async () => { queryFn: async () => {
return mediaType === MediaType.MOVIE return mediaType === MediaType.MOVIE
? jellyseerrApi?.movieRatings(result.id) ? seerrApi?.movieRatings(result.id)
: jellyseerrApi?.tvRatings(result.id); : seerrApi?.tvRatings(result.id);
}, },
staleTime: (5).minutesToMilliseconds(), staleTime: (5).minutesToMilliseconds(),
retry: false, retry: false,
enabled: !!jellyseerrApi, enabled: !!seerrApi,
}); });
return ( return (

View File

@@ -21,7 +21,7 @@ interface Props extends TouchableOpacityProps {
mediaType: MediaType; mediaType: MediaType;
} }
export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({ export const TouchableSeerrRouter: React.FC<PropsWithChildren<Props>> = ({
result, result,
mediaTitle, mediaTitle,
releaseYear, releaseYear,
@@ -43,7 +43,7 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
if (!result) return; if (!result) return;
router.push({ router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, pathname: `/(auth)/(tabs)/${from}/seerr/page`,
// @ts-expect-error // @ts-expect-error
params: { params: {
...result, ...result,

View File

@@ -23,10 +23,7 @@ export const MusicAlbumCard: React.FC<Props> = ({ album, width = 130 }) => {
); );
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
router.push({ router.push(`/music/album/${album.Id}`);
pathname: "/music/album/[albumId]",
params: { albumId: album.Id! },
});
}, [router, album.Id]); }, [router, album.Id]);
return ( return (

View File

@@ -24,10 +24,7 @@ export const MusicAlbumRowCard: React.FC<Props> = ({ album }) => {
); );
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
router.push({ router.push(`/music/album/${album.Id}`);
pathname: "/music/album/[albumId]",
params: { albumId: album.Id! },
});
}, [router, album.Id]); }, [router, album.Id]);
return ( return (

View File

@@ -25,10 +25,7 @@ export const MusicArtistCard: React.FC<Props> = ({ artist }) => {
); );
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
router.push({ router.push(`/music/artist/${artist.Id}`);
pathname: "/music/artist/[artistId]",
params: { artistId: artist.Id! },
});
}, [router, artist.Id]); }, [router, artist.Id]);
return ( return (

View File

@@ -61,10 +61,7 @@ export const MusicPlaylistCard: React.FC<Props> = ({ playlist }) => {
const hasDownloads = downloadStatus.downloaded > 0; const hasDownloads = downloadStatus.downloaded > 0;
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
router.push({ router.push(`/music/playlist/${playlist.Id}`);
pathname: "/music/playlist/[playlistId]",
params: { playlistId: playlist.Id! },
});
}, [router, playlist.Id]); }, [router, playlist.Id]);
return ( return (

View File

@@ -197,10 +197,7 @@ export const TrackOptionsSheet: React.FC<Props> = ({
const artistId = track?.ArtistItems?.[0]?.Id; const artistId = track?.ArtistItems?.[0]?.Id;
if (artistId) { if (artistId) {
setOpen(false); setOpen(false);
router.push({ router.push(`/music/artist/${artistId}`);
pathname: "/music/artist/[artistId]",
params: { artistId },
});
} }
}, [track?.ArtistItems, router, setOpen]); }, [track?.ArtistItems, router, setOpen]);
@@ -208,10 +205,7 @@ export const TrackOptionsSheet: React.FC<Props> = ({
const albumId = track?.AlbumId || track?.ParentId; const albumId = track?.AlbumId || track?.ParentId;
if (albumId) { if (albumId) {
setOpen(false); setOpen(false);
router.push({ router.push(`/music/album/${albumId}`);
pathname: "/music/album/[albumId]",
params: { albumId },
});
} }
}, [track?.AlbumId, track?.ParentId, router, setOpen]); }, [track?.AlbumId, track?.ParentId, router, setOpen]);

View File

@@ -7,15 +7,15 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter"; import { TouchableSeerrRouter } from "@/components/common/SeerrItemRouter";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Tag, Tags } from "@/components/GenreTags"; import { Tag, Tags } from "@/components/GenreTags";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; import { textShadowStyle } from "@/components/seerr/discover/GenericSlideCard";
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; import SeerrMediaIcon from "@/components/seerr/SeerrMediaIcon";
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; import SeerrStatusIcon from "@/components/seerr/SeerrStatusIcon";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; import { useSeerrCanRequest } from "@/utils/_seerr/useSeerrCanRequest";
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media"; import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker"; import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker";
@@ -34,13 +34,13 @@ interface Props extends ViewProps {
mediaRequest?: MediaRequest; mediaRequest?: MediaRequest;
} }
const JellyseerrPoster: React.FC<Props> = ({ const SeerrPoster: React.FC<Props> = ({
item, item,
horizontal, horizontal,
showDownloadInfo, showDownloadInfo,
mediaRequest, mediaRequest,
}) => { }) => {
const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr(); const { seerrApi, getTitle, getYear, getMediaType } = useSeerr();
const loadingOpacity = useSharedValue(1); const loadingOpacity = useSharedValue(1);
const imageOpacity = useSharedValue(0); const imageOpacity = useSharedValue(0);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -56,16 +56,13 @@ const JellyseerrPoster: React.FC<Props> = ({
const backdropSrc = useMemo( const backdropSrc = useMemo(
() => () =>
jellyseerrApi?.imageProxy( seerrApi?.imageProxy(item?.backdropPath, "w1920_and_h800_multi_faces"),
item?.backdropPath, [item, seerrApi, horizontal],
"w1920_and_h800_multi_faces",
),
[item, jellyseerrApi, horizontal],
); );
const posterSrc = useMemo( const posterSrc = useMemo(
() => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"), () => seerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"),
[item, jellyseerrApi, horizontal], [item, seerrApi, horizontal],
); );
const title = useMemo(() => getTitle(item), [item]); const title = useMemo(() => getTitle(item), [item]);
@@ -75,7 +72,7 @@ const JellyseerrPoster: React.FC<Props> = ({
const size = useMemo(() => (horizontal ? "h-28" : "w-28"), [horizontal]); const size = useMemo(() => (horizontal ? "h-28" : "w-28"), [horizontal]);
const ratio = useMemo(() => (horizontal ? "15/10" : "10/15"), [horizontal]); const ratio = useMemo(() => (horizontal ? "15/10" : "10/15"), [horizontal]);
const [canRequest] = useJellyseerrCanRequest(item); const [canRequest] = useSeerrCanRequest(item);
const is4k = useMemo(() => mediaRequest?.is4k === true, [mediaRequest]); const is4k = useMemo(() => mediaRequest?.is4k === true, [mediaRequest]);
@@ -109,7 +106,7 @@ const JellyseerrPoster: React.FC<Props> = ({
second, second,
third, third,
fourth, fourth,
t("home.settings.plugins.jellyseerr.plus_n_more", { n: rest.length }), t("home.settings.plugins.seerr.plus_n_more", { n: rest.length }),
]; ];
} }
return seasons; return seasons;
@@ -121,7 +118,7 @@ const JellyseerrPoster: React.FC<Props> = ({
}, [mediaRequest, is4k]); }, [mediaRequest, is4k]);
return ( return (
<TouchableJellyseerrRouter <TouchableSeerrRouter
result={item} result={item}
mediaTitle={title} mediaTitle={title}
releaseYear={releaseYear} releaseYear={releaseYear}
@@ -184,12 +181,12 @@ const JellyseerrPoster: React.FC<Props> = ({
)} )}
</> </>
)} )}
<JellyseerrStatusIcon <SeerrStatusIcon
className='absolute bottom-1 right-1' className='absolute bottom-1 right-1'
showRequestIcon={canRequest} showRequestIcon={canRequest}
mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status} mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
/> />
<JellyseerrMediaIcon <SeerrMediaIcon
className='absolute top-1 left-1' className='absolute top-1 left-1'
mediaType={mediaType} mediaType={mediaType}
/> />
@@ -201,8 +198,8 @@ const JellyseerrPoster: React.FC<Props> = ({
{releaseYear || ""} {releaseYear || ""}
</Text> </Text>
</View> </View>
</TouchableJellyseerrRouter> </TouchableSeerrRouter>
); );
}; };
export default JellyseerrPoster; export default SeerrPoster;

View File

@@ -1,19 +1,19 @@
import { Button, ContextMenu, Host, Picker } from "@expo/ui/swift-ui"; import { Button, ContextMenu, Host, Picker } from "@expo/ui/swift-ui";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { FilterButton } from "@/components/filters/FilterButton"; import { FilterButton } from "@/components/filters/FilterButton";
import { JellyseerrSearchSort } from "@/components/jellyseerr/JellyseerrIndexPage"; import { SeerrSearchSort } from "@/components/seerr/SeerrIndexPage";
interface DiscoverFiltersProps { interface DiscoverFiltersProps {
searchFilterId: string; searchFilterId: string;
orderFilterId: string; orderFilterId: string;
jellyseerrOrderBy: JellyseerrSearchSort; seerrOrderBy: SeerrSearchSort;
setJellyseerrOrderBy: (value: JellyseerrSearchSort) => void; setSeerrOrderBy: (value: SeerrSearchSort) => void;
jellyseerrSortOrder: "asc" | "desc"; seerrSortOrder: "asc" | "desc";
setJellyseerrSortOrder: (value: "asc" | "desc") => void; setSeerrSortOrder: (value: "asc" | "desc") => void;
t: (key: string) => string; t: (key: string) => string;
} }
const sortOptions = Object.keys(JellyseerrSearchSort).filter((v) => const sortOptions = Object.keys(SeerrSearchSort).filter((v) =>
Number.isNaN(Number(v)), Number.isNaN(Number(v)),
); );
@@ -22,10 +22,10 @@ const orderOptions = ["asc", "desc"] as const;
export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
searchFilterId, searchFilterId,
orderFilterId, orderFilterId,
jellyseerrOrderBy, seerrOrderBy,
setJellyseerrOrderBy, setSeerrOrderBy,
jellyseerrSortOrder, seerrSortOrder,
setJellyseerrSortOrder, setSeerrSortOrder,
t, t,
}) => { }) => {
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
@@ -52,16 +52,16 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
<Picker <Picker
label={t("library.filters.sort_by")} label={t("library.filters.sort_by")}
options={sortOptions.map((item) => options={sortOptions.map((item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`), t(`home.settings.plugins.seerr.order_by.${item}`),
)} )}
variant='menu' variant='menu'
selectedIndex={sortOptions.indexOf( selectedIndex={sortOptions.indexOf(
jellyseerrOrderBy as unknown as string, seerrOrderBy as unknown as string,
)} )}
onOptionSelected={(event: any) => { onOptionSelected={(event: any) => {
const index = event.nativeEvent.index; const index = event.nativeEvent.index;
setJellyseerrOrderBy( setSeerrOrderBy(
sortOptions[index] as unknown as JellyseerrSearchSort, sortOptions[index] as unknown as SeerrSearchSort,
); );
}} }}
/> />
@@ -69,10 +69,10 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
label={t("library.filters.sort_order")} label={t("library.filters.sort_order")}
options={orderOptions.map((item) => t(`library.filters.${item}`))} options={orderOptions.map((item) => t(`library.filters.${item}`))}
variant='menu' variant='menu'
selectedIndex={orderOptions.indexOf(jellyseerrSortOrder)} selectedIndex={orderOptions.indexOf(seerrSortOrder)}
onOptionSelected={(event: any) => { onOptionSelected={(event: any) => {
const index = event.nativeEvent.index; const index = event.nativeEvent.index;
setJellyseerrSortOrder(orderOptions[index]); setSeerrSortOrder(orderOptions[index]);
}} }}
/> />
</ContextMenu.Items> </ContextMenu.Items>
@@ -86,17 +86,15 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
<View className='flex flex-row justify-end items-center space-x-1'> <View className='flex flex-row justify-end items-center space-x-1'>
<FilterButton <FilterButton
id={searchFilterId} id={searchFilterId}
queryKey='jellyseerr_search' queryKey='seerr_search'
queryFn={async () => queryFn={async () =>
Object.keys(JellyseerrSearchSort).filter((v) => Object.keys(SeerrSearchSort).filter((v) => Number.isNaN(Number(v)))
Number.isNaN(Number(v)),
)
} }
set={(value) => setJellyseerrOrderBy(value[0])} set={(value) => setSeerrOrderBy(value[0])}
values={[jellyseerrOrderBy]} values={[seerrOrderBy]}
title={t("library.filters.sort_by")} title={t("library.filters.sort_by")}
renderItemLabel={(item) => renderItemLabel={(item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`) t(`home.settings.plugins.seerr.order_by.${item}`)
} }
disableSearch={true} disableSearch={true}
/> />
@@ -104,8 +102,8 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
id={orderFilterId} id={orderFilterId}
queryKey='jellysearr_search' queryKey='jellysearr_search'
queryFn={async () => ["asc", "desc"]} queryFn={async () => ["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])} set={(value) => setSeerrSortOrder(value[0])}
values={[jellyseerrSortOrder]} values={[seerrSortOrder]}
title={t("library.filters.sort_order")} title={t("library.filters.sort_order")}
renderItemLabel={(item) => t(`library.filters.${item}`)} renderItemLabel={(item) => t(`library.filters.${item}`)}
disableSearch={true} disableSearch={true}

View File

@@ -3,7 +3,7 @@ import type React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster"; import PersonPoster from "@/components/seerr/PersonPoster";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
@@ -15,9 +15,7 @@ const CastSlide: React.FC<
details?.credits?.cast && details?.credits?.cast &&
details?.credits?.cast?.length > 0 && ( details?.credits?.cast?.length > 0 && (
<View {...props}> <View {...props}>
<Text className='text-lg font-bold mb-2 px-4'> <Text className='text-lg font-bold mb-2 px-4'>{t("seerr.cast")}</Text>
{t("jellyseerr.cast")}
</Text>
<FlashList <FlashList
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}

View File

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import CountryFlag from "react-native-country-flag"; import CountryFlag from "react-native-country-flag";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces"; import type { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
@@ -50,8 +50,7 @@ const Fact: React.FC<{ title: string; fact?: string | null } & ViewProps> = ({
const DetailFacts: React.FC< const DetailFacts: React.FC<
{ details?: MovieDetails | TvDetails } & ViewProps { details?: MovieDetails | TvDetails } & ViewProps
> = ({ details, className, ...props }) => { > = ({ details, className, ...props }) => {
const { jellyseerrRegion: region, jellyseerrLocale: locale } = const { seerrRegion: region, seerrLocale: locale } = useSeerr();
useJellyseerr();
const { t } = useTranslation(); const { t } = useTranslation();
const releases = useMemo( const releases = useMemo(
@@ -138,21 +137,21 @@ const DetailFacts: React.FC<
return ( return (
details && ( details && (
<View className='p-4'> <View className='p-4'>
<Text className='text-lg font-bold'>{t("jellyseerr.details")}</Text> <Text className='text-lg font-bold'>{t("seerr.details")}</Text>
<View <View
className={`${className} flex flex-col justify-center divide-y-2 divide-neutral-800`} className={`${className} flex flex-col justify-center divide-y-2 divide-neutral-800`}
{...props} {...props}
> >
<Fact title={t("jellyseerr.status")} fact={details?.status} /> <Fact title={t("seerr.status")} fact={details?.status} />
<Fact <Fact
title={t("jellyseerr.original_title")} title={t("seerr.original_title")}
fact={(details as TvDetails)?.originalName} fact={(details as TvDetails)?.originalName}
/> />
{details.keywords.some( {details.keywords.some(
(keyword) => keyword.id === ANIME_KEYWORD_ID, (keyword) => keyword.id === ANIME_KEYWORD_ID,
) && <Fact title={t("jellyseerr.series_type")} fact='Anime' />} ) && <Fact title={t("seerr.series_type")} fact='Anime' />}
<Facts <Facts
title={t("jellyseerr.release_dates")} title={t("seerr.release_dates")}
facts={filteredReleases?.map?.((r: Release, idx) => ( 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 ? ( {r.type === 3 ? (
@@ -178,16 +177,13 @@ const DetailFacts: React.FC<
</View> </View>
))} ))}
/> />
<Fact title={t("jellyseerr.first_air_date")} fact={firstAirDate} /> <Fact title={t("seerr.first_air_date")} fact={firstAirDate} />
<Fact title={t("jellyseerr.next_air_date")} fact={nextAirDate} /> <Fact title={t("seerr.next_air_date")} fact={nextAirDate} />
<Fact title={t("jellyseerr.revenue")} fact={revenue} /> <Fact title={t("seerr.revenue")} fact={revenue} />
<Fact title={t("jellyseerr.budget")} fact={budget} /> <Fact title={t("seerr.budget")} fact={budget} />
<Fact <Fact title={t("seerr.original_language")} fact={spokenLanguage} />
title={t("jellyseerr.original_language")}
fact={spokenLanguage}
/>
<Facts <Facts
title={t("jellyseerr.production_country")} title={t("seerr.production_country")}
facts={details?.productionCountries?.map((n, idx) => ( 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} /> <CountryFlag isoCode={n.iso_3166_1} size={10} />
@@ -196,17 +192,17 @@ const DetailFacts: React.FC<
))} ))}
/> />
<Facts <Facts
title={t("jellyseerr.studios")} title={t("seerr.studios")}
facts={uniqBy(details?.productionCompanies, "name")?.map( facts={uniqBy(details?.productionCompanies, "name")?.map(
(n) => n.name, (n) => n.name,
)} )}
/> />
<Facts <Facts
title={t("jellyseerr.network")} title={t("seerr.network")}
facts={networks?.map((n) => n.name)} facts={networks?.map((n) => n.name)}
/> />
<Facts <Facts
title={t("jellyseerr.currently_streaming_on")} title={t("seerr.currently_streaming_on")}
facts={streamingProviders?.map((s) => s.name)} facts={streamingProviders?.map((s) => s.name)}
/> />
</View> </View>

View File

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

View File

@@ -12,7 +12,7 @@ import { View, type ViewProps } from "react-native";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { PlatformDropdown } from "@/components/PlatformDropdown"; import { PlatformDropdown } from "@/components/PlatformDropdown";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import type { import type {
QualityProfile, QualityProfile,
RootFolder, RootFolder,
@@ -41,11 +41,11 @@ const RequestModal = forwardRef<
{ id, title, requestBody, type, isAnime = false, onRequested, onDismiss }, { id, title, requestBody, type, isAnime = false, onRequested, onDismiss },
ref, ref,
) => { ) => {
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); const { seerrApi, seerrUser, requestMedia } = useSeerr();
const [requestOverrides, setRequestOverrides] = useState<MediaRequestBody>({ const [requestOverrides, setRequestOverrides] = useState<MediaRequestBody>({
mediaId: Number(id), mediaId: Number(id),
mediaType: type, mediaType: type,
userId: jellyseerrUser?.id, userId: seerrUser?.id,
}); });
const [qualityProfileOpen, setQualityProfileOpen] = useState(false); const [qualityProfileOpen, setQualityProfileOpen] = useState(false);
@@ -65,18 +65,17 @@ const RequestModal = forwardRef<
}, [onDismiss]); }, [onDismiss]);
const { data: serviceSettings } = useQuery({ const { data: serviceSettings } = useQuery({
queryKey: ["jellyseerr", "request", type, "service"], queryKey: ["seerr", "request", type, "service"],
queryFn: async () => queryFn: async () =>
jellyseerrApi?.service(type === "movie" ? "radarr" : "sonarr"), seerrApi?.service(type === "movie" ? "radarr" : "sonarr"),
enabled: !!jellyseerrApi && !!jellyseerrUser, enabled: !!seerrApi && !!seerrUser,
refetchOnMount: "always", refetchOnMount: "always",
}); });
const { data: users } = useQuery({ const { data: users } = useQuery({
queryKey: ["jellyseerr", "users"], queryKey: ["seerr", "users"],
queryFn: async () => queryFn: async () => seerrApi?.user({ take: 1000, sort: "displayname" }),
jellyseerrApi?.user({ take: 1000, sort: "displayname" }), enabled: !!seerrApi && !!seerrUser,
enabled: !!jellyseerrApi && !!jellyseerrUser,
refetchOnMount: "always", refetchOnMount: "always",
}); });
@@ -87,7 +86,7 @@ const RequestModal = forwardRef<
const { data: defaultServiceDetails } = useQuery({ const { data: defaultServiceDetails } = useQuery({
queryKey: [ queryKey: [
"jellyseerr", "seerr",
"request", "request",
type, type,
"service", "service",
@@ -99,12 +98,12 @@ const RequestModal = forwardRef<
...prev, ...prev,
serverId: defaultService?.id, serverId: defaultService?.id,
})); }));
return jellyseerrApi?.serviceDetails( return seerrApi?.serviceDetails(
type === "movie" ? "radarr" : "sonarr", type === "movie" ? "radarr" : "sonarr",
defaultService!.id, defaultService!.id,
); );
}, },
enabled: !!jellyseerrApi && !!jellyseerrUser && !!defaultService, enabled: !!seerrApi && !!seerrUser && !!defaultService,
refetchOnMount: "always", refetchOnMount: "always",
}); });
@@ -148,9 +147,9 @@ const RequestModal = forwardRef<
return undefined; return undefined;
} }
if (requestBody.seasons.length > 1) { if (requestBody.seasons.length > 1) {
return t("jellyseerr.season_all"); return t("seerr.season_all");
} }
return t("jellyseerr.season_number", { return t("seerr.season_number", {
season_number: requestBody.seasons[0], season_number: requestBody.seasons[0],
}); });
}, [requestBody?.seasons]); }, [requestBody?.seasons]);
@@ -245,8 +244,7 @@ const RequestModal = forwardRef<
type: "radio" as const, type: "radio" as const,
label: user.displayName, label: user.displayName,
value: user.id.toString(), value: user.id.toString(),
selected: selected: (requestOverrides.userId || seerrUser?.id) === user.id,
(requestOverrides.userId || jellyseerrUser?.id) === user.id,
onPress: () => onPress: () =>
setRequestOverrides((prev) => ({ setRequestOverrides((prev) => ({
...prev, ...prev,
@@ -255,7 +253,7 @@ const RequestModal = forwardRef<
})) || [], })) || [],
}, },
], ],
[users, jellyseerrUser, requestOverrides.userId], [users, seerrUser, requestOverrides.userId],
); );
const request = useCallback(() => { const request = useCallback(() => {
@@ -268,7 +266,7 @@ const RequestModal = forwardRef<
...requestOverrides, ...requestOverrides,
}; };
writeDebugLog("Sending Jellyseerr advanced request", body); writeDebugLog("Sending Seerr advanced request", body);
requestMedia( requestMedia(
seasonTitle ? `${title}, ${seasonTitle}` : title, seasonTitle ? `${title}, ${seasonTitle}` : title,
@@ -308,7 +306,7 @@ const RequestModal = forwardRef<
<View className='flex flex-col space-y-4 px-4 pb-8 pt-2'> <View className='flex flex-col space-y-4 px-4 pb-8 pt-2'>
<View> <View>
<Text className='font-bold text-2xl text-neutral-100'> <Text className='font-bold text-2xl text-neutral-100'>
{t("jellyseerr.advanced")} {t("seerr.advanced")}
</Text> </Text>
{seasonTitle && ( {seasonTitle && (
<Text className='text-neutral-300'>{seasonTitle}</Text> <Text className='text-neutral-300'>{seasonTitle}</Text>
@@ -319,7 +317,7 @@ const RequestModal = forwardRef<
<> <>
<View className='flex flex-col'> <View className='flex flex-col'>
<Text className='opacity-50 mb-1 text-xs'> <Text className='opacity-50 mb-1 text-xs'>
{t("jellyseerr.quality_profile")} {t("seerr.quality_profile")}
</Text> </Text>
<PlatformDropdown <PlatformDropdown
groups={qualityProfileOptions} groups={qualityProfileOptions}
@@ -335,7 +333,7 @@ const RequestModal = forwardRef<
</Text> </Text>
</View> </View>
} }
title={t("jellyseerr.quality_profile")} title={t("seerr.quality_profile")}
open={qualityProfileOpen} open={qualityProfileOpen}
onOpenChange={setQualityProfileOpen} onOpenChange={setQualityProfileOpen}
/> />
@@ -343,7 +341,7 @@ const RequestModal = forwardRef<
<View className='flex flex-col'> <View className='flex flex-col'>
<Text className='opacity-50 mb-1 text-xs'> <Text className='opacity-50 mb-1 text-xs'>
{t("jellyseerr.root_folder")} {t("seerr.root_folder")}
</Text> </Text>
<PlatformDropdown <PlatformDropdown
groups={rootFolderOptions} groups={rootFolderOptions}
@@ -368,7 +366,7 @@ const RequestModal = forwardRef<
</Text> </Text>
</View> </View>
} }
title={t("jellyseerr.root_folder")} title={t("seerr.root_folder")}
open={rootFolderOpen} open={rootFolderOpen}
onOpenChange={setRootFolderOpen} onOpenChange={setRootFolderOpen}
/> />
@@ -376,7 +374,7 @@ const RequestModal = forwardRef<
<View className='flex flex-col'> <View className='flex flex-col'>
<Text className='opacity-50 mb-1 text-xs'> <Text className='opacity-50 mb-1 text-xs'>
{t("jellyseerr.tags")} {t("seerr.tags")}
</Text> </Text>
<PlatformDropdown <PlatformDropdown
groups={tagsOptions} groups={tagsOptions}
@@ -395,7 +393,7 @@ const RequestModal = forwardRef<
</Text> </Text>
</View> </View>
} }
title={t("jellyseerr.tags")} title={t("seerr.tags")}
open={tagsOpen} open={tagsOpen}
onOpenChange={setTagsOpen} onOpenChange={setTagsOpen}
/> />
@@ -403,7 +401,7 @@ const RequestModal = forwardRef<
<View className='flex flex-col'> <View className='flex flex-col'>
<Text className='opacity-50 mb-1 text-xs'> <Text className='opacity-50 mb-1 text-xs'>
{t("jellyseerr.request_as")} {t("seerr.request_as")}
</Text> </Text>
<PlatformDropdown <PlatformDropdown
groups={usersOptions} groups={usersOptions}
@@ -413,12 +411,12 @@ const RequestModal = forwardRef<
{users.find( {users.find(
(u) => (u) =>
u.id === u.id ===
(requestOverrides.userId || jellyseerrUser?.id), (requestOverrides.userId || seerrUser?.id),
)?.displayName || jellyseerrUser!.displayName} )?.displayName || seerrUser!.displayName}
</Text> </Text>
</View> </View>
} }
title={t("jellyseerr.request_as")} title={t("seerr.request_as")}
open={usersOpen} open={usersOpen}
onOpenChange={setUsersOpen} onOpenChange={setUsersOpen}
/> />
@@ -427,7 +425,7 @@ const RequestModal = forwardRef<
)} )}
</View> </View>
<Button className='mt-auto' onPress={request} color='purple'> <Button className='mt-auto' onPress={request} color='purple'>
{t("jellyseerr.request_button")} {t("seerr.request_button")}
</Button> </Button>
</View> </View>
</BottomSheetView> </BottomSheetView>

View File

@@ -8,8 +8,8 @@ import {
useSharedValue, useSharedValue,
withTiming, withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import Discover from "@/components/jellyseerr/discover/Discover"; import Discover from "@/components/seerr/discover/Discover";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { import type {
MovieResult, MovieResult,
@@ -18,57 +18,57 @@ import type {
} from "@/utils/jellyseerr/server/models/Search"; } from "@/utils/jellyseerr/server/models/Search";
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery"; import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import JellyseerrPoster from "../posters/JellyseerrPoster"; import SeerrPoster from "../posters/SeerrPoster";
import { LoadingSkeleton } from "../search/LoadingSkeleton"; import { LoadingSkeleton } from "../search/LoadingSkeleton";
import { SearchItemWrapper } from "../search/SearchItemWrapper"; import { SearchItemWrapper } from "../search/SearchItemWrapper";
import PersonPoster from "./PersonPoster"; import PersonPoster from "./PersonPoster";
interface Props extends ViewProps { interface Props extends ViewProps {
searchQuery: string; searchQuery: string;
sortType?: JellyseerrSearchSort; sortType?: SeerrSearchSort;
order?: "asc" | "desc"; order?: "asc" | "desc";
} }
export enum JellyseerrSearchSort { export enum SeerrSearchSort {
DEFAULT = 0, DEFAULT = 0,
VOTE_COUNT_AND_AVERAGE = 1, VOTE_COUNT_AND_AVERAGE = 1,
POPULARITY = 2, POPULARITY = 2,
} }
export const JellyserrIndexPage: React.FC<Props> = ({ export const SeerrIndexPage: React.FC<Props> = ({
searchQuery, searchQuery,
sortType, sortType,
order, order,
}) => { }) => {
const { jellyseerrApi } = useJellyseerr(); const { seerrApi } = useSeerr();
const opacity = useSharedValue(1); const opacity = useSharedValue(1);
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
data: jellyseerrDiscoverSettings, data: seerrDiscoverSettings,
isFetching: f1, isFetching: f1,
isLoading: l1, isLoading: l1,
} = useReactNavigationQuery({ } = useReactNavigationQuery({
queryKey: ["search", "jellyseerr", "discoverSettings", searchQuery], queryKey: ["search", "seerr", "discoverSettings", searchQuery],
queryFn: async () => jellyseerrApi?.discoverSettings(), queryFn: async () => seerrApi?.discoverSettings(),
enabled: !!jellyseerrApi && searchQuery.length === 0, enabled: !!seerrApi && searchQuery.length === 0,
}); });
const { const {
data: jellyseerrResults, data: seerrResults,
isFetching: f2, isFetching: f2,
isLoading: l2, isLoading: l2,
} = useReactNavigationQuery({ } = useReactNavigationQuery({
queryKey: ["search", "jellyseerr", "results", searchQuery], queryKey: ["search", "seerr", "results", searchQuery],
queryFn: async () => { queryFn: async () => {
const params = { const params = {
query: new URLSearchParams(searchQuery || "").toString(), query: new URLSearchParams(searchQuery || "").toString(),
}; };
return await Promise.all([ return await Promise.all([
jellyseerrApi?.search({ ...params, page: 1 }), seerrApi?.search({ ...params, page: 1 }),
jellyseerrApi?.search({ ...params, page: 2 }), seerrApi?.search({ ...params, page: 2 }),
jellyseerrApi?.search({ ...params, page: 3 }), seerrApi?.search({ ...params, page: 3 }),
jellyseerrApi?.search({ ...params, page: 4 }), seerrApi?.search({ ...params, page: 4 }),
]).then((all) => ]).then((all) =>
uniqBy( uniqBy(
all.flatMap((v) => v?.results || []), all.flatMap((v) => v?.results || []),
@@ -76,7 +76,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
), ),
); );
}, },
enabled: !!jellyseerrApi && searchQuery.length > 0, enabled: !!seerrApi && searchQuery.length > 0,
}); });
useAnimatedReaction( useAnimatedReaction(
@@ -92,20 +92,20 @@ export const JellyserrIndexPage: React.FC<Props> = ({
const sortingType = useMemo(() => { const sortingType = useMemo(() => {
if (!sortType) return; if (!sortType) return;
switch (Number(JellyseerrSearchSort[sortType])) { switch (Number(SeerrSearchSort[sortType])) {
case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE: case SeerrSearchSort.VOTE_COUNT_AND_AVERAGE:
return ["voteCount", "voteAverage"]; return ["voteCount", "voteAverage"];
case JellyseerrSearchSort.POPULARITY: case SeerrSearchSort.POPULARITY:
return ["voteCount", "popularity"]; return ["voteCount", "popularity"];
default: default:
return undefined; return undefined;
} }
}, [sortType, order]); }, [sortType, order]);
const jellyseerrMovieResults = useMemo( const seerrMovieResults = useMemo(
() => () =>
orderBy( orderBy(
jellyseerrResults?.filter( seerrResults?.filter(
(r) => r.mediaType === MediaType.MOVIE, (r) => r.mediaType === MediaType.MOVIE,
) as MovieResult[], ) as MovieResult[],
sortingType || [ sortingType || [
@@ -113,41 +113,37 @@ export const JellyserrIndexPage: React.FC<Props> = ({
], ],
order || "desc", order || "desc",
), ),
[jellyseerrResults, sortingType, order], [seerrResults, sortingType, order],
); );
const jellyseerrTvResults = useMemo( const seerrTvResults = useMemo(
() => () =>
orderBy( orderBy(
jellyseerrResults?.filter( seerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[],
(r) => r.mediaType === MediaType.TV,
) as TvResult[],
sortingType || [ sortingType || [
(t) => t.name.toLowerCase() === searchQuery.toLowerCase(), (t) => t.name.toLowerCase() === searchQuery.toLowerCase(),
], ],
order || "desc", order || "desc",
), ),
[jellyseerrResults, sortingType, order], [seerrResults, sortingType, order],
); );
const jellyseerrPersonResults = useMemo( const seerrPersonResults = useMemo(
() => () =>
orderBy( orderBy(
jellyseerrResults?.filter( seerrResults?.filter((r) => r.mediaType === "person") as PersonResult[],
(r) => r.mediaType === "person",
) as PersonResult[],
sortingType || [ sortingType || [
(p) => p.name.toLowerCase() === searchQuery.toLowerCase(), (p) => p.name.toLowerCase() === searchQuery.toLowerCase(),
], ],
order || "desc", order || "desc",
), ),
[jellyseerrResults, sortingType, order], [seerrResults, sortingType, order],
); );
if (!searchQuery.length) if (!searchQuery.length)
return ( return (
<View className='flex flex-col'> <View className='flex flex-col'>
<Discover sliders={jellyseerrDiscoverSettings} /> <Discover sliders={seerrDiscoverSettings} />
</View> </View>
); );
@@ -155,9 +151,9 @@ export const JellyserrIndexPage: React.FC<Props> = ({
<View> <View>
<LoadingSkeleton isLoading={f1 || f2 || l1 || l2} /> <LoadingSkeleton isLoading={f1 || f2 || l1 || l2} />
{!jellyseerrMovieResults?.length && {!seerrMovieResults?.length &&
!jellyseerrTvResults?.length && !seerrTvResults?.length &&
!jellyseerrPersonResults?.length && !seerrPersonResults?.length &&
!f1 && !f1 &&
!f2 && !f2 &&
!l1 && !l1 &&
@@ -175,21 +171,21 @@ export const JellyserrIndexPage: React.FC<Props> = ({
<View className={f1 || f2 || l1 || l2 ? "opacity-0" : "opacity-100"}> <View className={f1 || f2 || l1 || l2 ? "opacity-0" : "opacity-100"}>
<SearchItemWrapper <SearchItemWrapper
header={t("search.request_movies")} header={t("search.request_movies")}
items={jellyseerrMovieResults} items={seerrMovieResults}
renderItem={(item: MovieResult) => ( renderItem={(item: MovieResult) => (
<JellyseerrPoster item={item} key={item.id} /> <SeerrPoster item={item} key={item.id} />
)} )}
/> />
<SearchItemWrapper <SearchItemWrapper
header={t("search.request_series")} header={t("search.request_series")}
items={jellyseerrTvResults} items={seerrTvResults}
renderItem={(item: TvResult) => ( renderItem={(item: TvResult) => (
<JellyseerrPoster item={item} key={item.id} /> <SeerrPoster item={item} key={item.id} />
)} )}
/> />
<SearchItemWrapper <SearchItemWrapper
header={t("search.actors")} header={t("search.actors")}
items={jellyseerrPersonResults} items={seerrPersonResults}
renderItem={(item: PersonResult) => ( renderItem={(item: PersonResult) => (
<PersonPoster <PersonPoster
className='mr-2' className='mr-2'

View File

@@ -3,9 +3,11 @@ import { useMemo } from "react";
import { View, type ViewProps } from "react-native"; import { View, type ViewProps } from "react-native";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
const JellyseerrMediaIcon: React.FC< const SeerrMediaIcon: React.FC<{ mediaType: "tv" | "movie" } & ViewProps> = ({
{ mediaType: "tv" | "movie" } & ViewProps mediaType,
> = ({ mediaType, className, ...props }) => { className,
...props
}) => {
const style = useMemo( const style = useMemo(
() => () =>
mediaType === MediaType.MOVIE mediaType === MediaType.MOVIE
@@ -29,4 +31,4 @@ const JellyseerrMediaIcon: React.FC<
); );
}; };
export default JellyseerrMediaIcon; export default SeerrMediaIcon;

View File

@@ -9,7 +9,7 @@ interface Props {
onPress?: () => void; onPress?: () => void;
} }
const JellyseerrStatusIcon: React.FC<Props & ViewProps> = ({ const SeerrStatusIcon: React.FC<Props & ViewProps> = ({
mediaStatus, mediaStatus,
showRequestIcon, showRequestIcon,
onPress, onPress,
@@ -74,4 +74,4 @@ const JellyseerrStatusIcon: React.FC<Props & ViewProps> = ({
); );
}; };
export default JellyseerrStatusIcon; export default SeerrStatusIcon;

View File

@@ -2,10 +2,10 @@ import { useSegments } from "expo-router";
import type React from "react"; import type React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native"; import { TouchableOpacity, type ViewProps } from "react-native";
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard"; import GenericSlideCard from "@/components/seerr/discover/GenericSlideCard";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import Slide, { type SlideProps } from "@/components/seerr/discover/Slide";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { import {
COMPANY_LOGO_IMAGE_FILTER, COMPANY_LOGO_IMAGE_FILTER,
type Network, type Network,
@@ -16,14 +16,14 @@ const CompanySlide: React.FC<
{ data: Network[] | Studio[] } & SlideProps & ViewProps { data: Network[] | Studio[] } & SlideProps & ViewProps
> = ({ slide, data, ...props }) => { > = ({ slide, data, ...props }) => {
const segments = useSegments(); const segments = useSegments();
const { jellyseerrApi } = useJellyseerr(); const { seerrApi } = useSeerr();
const router = useRouter(); const router = useRouter();
const from = (segments as string[])[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
const navigate = useCallback( const navigate = useCallback(
({ id, image, name }: Network | Studio) => ({ id, image, name }: Network | Studio) =>
router.push({ router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}` as any, pathname: `/(auth)/(tabs)/${from}/seerr/company/${id}` as any,
params: { id, image, name, type: slide.type }, params: { id, image, name, type: slide.type },
}), }),
[slide], [slide],
@@ -40,10 +40,7 @@ const CompanySlide: React.FC<
<GenericSlideCard <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()} id={item.id.toString()}
url={jellyseerrApi?.imageProxy( url={seerrApi?.imageProxy(item.image, COMPANY_LOGO_IMAGE_FILTER)}
item.image,
COMPANY_LOGO_IMAGE_FILTER,
)}
/> />
</TouchableOpacity> </TouchableOpacity>
)} )}

View File

@@ -2,10 +2,10 @@ import { sortBy } from "lodash";
import type React from "react"; import type React from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import { View } from "react-native"; import { View } from "react-native";
import CompanySlide from "@/components/jellyseerr/discover/CompanySlide"; import CompanySlide from "@/components/seerr/discover/CompanySlide";
import GenreSlide from "@/components/jellyseerr/discover/GenreSlide"; import GenreSlide from "@/components/seerr/discover/GenreSlide";
import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide"; import MovieTvSlide from "@/components/seerr/discover/MovieTvSlide";
import RecentRequestsSlide from "@/components/jellyseerr/discover/RecentRequestsSlide"; import RecentRequestsSlide from "@/components/seerr/discover/RecentRequestsSlide";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { networks } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; import { networks } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";

View File

@@ -3,39 +3,39 @@ import { useSegments } from "expo-router";
import type React from "react"; import type React from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native"; import { TouchableOpacity, type ViewProps } from "react-native";
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard"; import GenericSlideCard from "@/components/seerr/discover/GenericSlideCard";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import Slide, { type SlideProps } from "@/components/seerr/discover/Slide";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; import { Endpoints, useSeerr } from "@/hooks/useSeerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import type { 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 { genreColorMap } from "@/utils/jellyseerr/src/components/Discover/constants";
const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => { const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
const segments = useSegments(); const segments = useSegments();
const { jellyseerrApi } = useJellyseerr(); const { seerrApi } = useSeerr();
const router = useRouter(); const router = useRouter();
const from = (segments as string[])[2] || "(home)"; const from = (segments as string[])[2] || "(home)";
const navigate = useCallback( const navigate = useCallback(
(genre: GenreSliderItem) => (genre: GenreSliderItem) =>
router.push({ router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}` as any, pathname: `/(auth)/(tabs)/${from}/seerr/genre/${genre.id}` as any,
params: { type: slide.type, name: genre.name }, params: { type: slide.type, name: genre.name },
}), }),
[slide], [slide],
); );
const { data } = useQuery({ const { data } = useQuery({
queryKey: ["jellyseerr", "discover", slide.type, slide.id], queryKey: ["seerr", "discover", slide.type, slide.id],
queryFn: async () => { queryFn: async () => {
return jellyseerrApi?.getGenreSliders( return seerrApi?.getGenreSliders(
slide.type === DiscoverSliderType.MOVIE_GENRES slide.type === DiscoverSliderType.MOVIE_GENRES
? Endpoints.MOVIE ? Endpoints.MOVIE
: Endpoints.TV, : Endpoints.TV,
); );
}, },
enabled: !!jellyseerrApi, enabled: !!seerrApi,
}); });
return ( return (
@@ -53,7 +53,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
title={item.name} title={item.name}
colors={["transparent", "transparent"]} colors={["transparent", "transparent"]}
contentFit={"cover"} contentFit={"cover"}
url={jellyseerrApi?.imageProxy( url={seerrApi?.imageProxy(
item.backdrops?.[0], item.backdrops?.[0],
`w780_filter(duotone,${ `w780_filter(duotone,${
genreColorMap[item.id] ?? genreColorMap[0] genreColorMap[item.id] ?? genreColorMap[0]

View File

@@ -3,23 +3,19 @@ import { uniqBy } from "lodash";
import type React from "react"; import type React from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import type { ViewProps } from "react-native"; import type { ViewProps } from "react-native";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import SeerrPoster from "@/components/posters/SeerrPoster";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import Slide, { type SlideProps } from "@/components/seerr/discover/Slide";
import { import { type DiscoverEndpoint, Endpoints, useSeerr } from "@/hooks/useSeerr";
type DiscoverEndpoint,
Endpoints,
useJellyseerr,
} from "@/hooks/useJellyseerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
slide, slide,
...props ...props
}) => { }) => {
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); const { seerrApi, isSeerrMovieOrTvResult } = useSeerr();
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "discover", slide.id], queryKey: ["seerr", "discover", slide.id],
queryFn: async ({ pageParam }) => { queryFn: async ({ pageParam }) => {
let endpoint: DiscoverEndpoint | undefined; let endpoint: DiscoverEndpoint | undefined;
let params: any = { let params: any = {
@@ -50,13 +46,13 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
break; break;
} }
return endpoint ? jellyseerrApi?.discover(endpoint, params) : null; return endpoint ? seerrApi?.discover(endpoint, params) : null;
}, },
initialPageParam: 1, initialPageParam: 1,
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
(lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) + (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
1, 1,
enabled: !!jellyseerrApi, enabled: !!seerrApi,
staleTime: 0, staleTime: 0,
}); });
@@ -65,9 +61,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
uniqBy( uniqBy(
data?.pages data?.pages
?.filter((p) => p?.results.length) ?.filter((p) => p?.results.length)
.flatMap((p) => .flatMap((p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r))),
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)),
),
"id", "id",
), ),
[data], [data],
@@ -84,7 +78,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
onEndReached={() => { onEndReached={() => {
if (hasNextPage) fetchNextPage(); if (hasNextPage) fetchNextPage();
}} }}
renderItem={(item) => <JellyseerrPoster item={item} key={item?.id} />} renderItem={(item) => <SeerrPoster item={item} key={item?.id} />}
/> />
) )
); );

View File

@@ -1,9 +1,9 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import type React from "react"; import type React from "react";
import type { ViewProps } from "react-native"; import type { ViewProps } from "react-native";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide"; import SeerrPoster from "@/components/posters/SeerrPoster";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import Slide, { type SlideProps } from "@/components/seerr/discover/Slide";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces/api/common"; import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces/api/common";
@@ -16,36 +16,36 @@ type ExtendedMediaRequest = NonFunctionProperties<MediaRequest> & {
const RequestCard: React.FC<{ request: ExtendedMediaRequest }> = ({ const RequestCard: React.FC<{ request: ExtendedMediaRequest }> = ({
request, request,
}) => { }) => {
const { jellyseerrApi } = useJellyseerr(); const { seerrApi } = useSeerr();
const { data: details } = useQuery({ const { data: details } = useQuery({
queryKey: [ queryKey: [
"jellyseerr", "seerr",
"detail", "detail",
request.media.mediaType, request.media.mediaType,
request.media.tmdbId, request.media.tmdbId,
], ],
queryFn: async () => { queryFn: async () => {
return request.media.mediaType === MediaType.MOVIE return request.media.mediaType === MediaType.MOVIE
? jellyseerrApi?.movieDetails(request.media.tmdbId) ? seerrApi?.movieDetails(request.media.tmdbId)
: jellyseerrApi?.tvDetails(request.media.tmdbId); : seerrApi?.tvDetails(request.media.tmdbId);
}, },
enabled: !!jellyseerrApi, enabled: !!seerrApi,
refetchOnMount: true, refetchOnMount: true,
staleTime: 0, staleTime: 0,
}); });
const { data: refreshedRequest } = useQuery({ const { data: refreshedRequest } = useQuery({
queryKey: ["jellyseerr", "requests", request.media.mediaType, request.id], queryKey: ["seerr", "requests", request.media.mediaType, request.id],
queryFn: async () => jellyseerrApi?.getRequest(request.id), queryFn: async () => seerrApi?.getRequest(request.id),
enabled: !!jellyseerrApi, enabled: !!seerrApi,
refetchOnMount: true, refetchOnMount: true,
refetchInterval: 5000, refetchInterval: 5000,
staleTime: 0, staleTime: 0,
}); });
return ( return (
<JellyseerrPoster <SeerrPoster
horizontal horizontal
showDownloadInfo showDownloadInfo
item={details} item={details}
@@ -58,12 +58,12 @@ const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({
slide, slide,
...props ...props
}) => { }) => {
const { jellyseerrApi } = useJellyseerr(); const { seerrApi } = useSeerr();
const { data: requests } = useQuery({ const { data: requests } = useQuery({
queryKey: ["jellyseerr", "recent_requests"], queryKey: ["seerr", "recent_requests"],
queryFn: async () => jellyseerrApi?.requests(), queryFn: async () => seerrApi?.requests(),
enabled: !!jellyseerrApi, enabled: !!seerrApi,
refetchOnMount: true, refetchOnMount: true,
staleTime: 0, staleTime: 0,
}); });

View File

@@ -14,11 +14,11 @@ import { Alert, TouchableOpacity, View } from "react-native";
import { HorizontalScroll } from "@/components/common/HorizontalScroll"; import { HorizontalScroll } from "@/components/common/HorizontalScroll";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { Tags } from "@/components/GenreTags"; import { Tags } from "@/components/GenreTags";
import { dateOpts } from "@/components/jellyseerr/DetailFacts";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
import { RoundButton } from "@/components/RoundButton"; import { RoundButton } from "@/components/RoundButton";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { dateOpts } from "@/components/seerr/DetailFacts";
import { textShadowStyle } from "@/components/seerr/discover/GenericSlideCard";
import SeerrStatusIcon from "@/components/seerr/SeerrStatusIcon";
import { useSeerr } from "@/hooks/useSeerr";
import { import {
MediaStatus, MediaStatus,
MediaType, MediaType,
@@ -30,15 +30,15 @@ import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import type { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import { Loader } from "../Loader"; import { Loader } from "../Loader";
const JellyseerrSeasonEpisodes: React.FC<{ const SeerrSeasonEpisodes: React.FC<{
details: TvDetails; details: TvDetails;
seasonNumber: number; seasonNumber: number;
}> = ({ details, seasonNumber }) => { }> = ({ details, seasonNumber }) => {
const { jellyseerrApi } = useJellyseerr(); const { seerrApi } = useSeerr();
const { data: seasonWithEpisodes, isLoading } = useQuery({ const { data: seasonWithEpisodes, isLoading } = useQuery({
queryKey: ["jellyseerr", details.id, "season", seasonNumber], queryKey: ["seerr", details.id, "season", seasonNumber],
queryFn: async () => jellyseerrApi?.tvSeason(details.id, seasonNumber), queryFn: async () => seerrApi?.tvSeason(details.id, seasonNumber),
enabled: details.seasons.filter((s) => s.seasonNumber !== 0).length > 0, enabled: details.seasons.filter((s) => s.seasonNumber !== 0).length > 0,
}); });
@@ -57,11 +57,7 @@ const JellyseerrSeasonEpisodes: React.FC<{
}; };
const RenderItem = ({ item }: any) => { const RenderItem = ({ item }: any) => {
const { const { seerrApi, seerrRegion: region, seerrLocale: locale } = useSeerr();
jellyseerrApi,
jellyseerrRegion: region,
jellyseerrLocale: locale,
} = useJellyseerr();
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
const upcomingAirDate = useMemo(() => { const upcomingAirDate = useMemo(() => {
@@ -83,7 +79,7 @@ const RenderItem = ({ item }: any) => {
key={item.id} key={item.id}
id={item.id} id={item.id}
source={{ source={{
uri: jellyseerrApi?.imageProxy(item.stillPath), uri: seerrApi?.imageProxy(item.stillPath),
}} }}
cachePolicy={"memory-disk"} cachePolicy={"memory-disk"}
contentFit='cover' contentFit='cover'
@@ -131,7 +127,7 @@ const RenderItem = ({ item }: any) => {
); );
}; };
const JellyseerrSeasons: React.FC<{ const SeerrSeasons: React.FC<{
isLoading: boolean; isLoading: boolean;
details?: TvDetails; details?: TvDetails;
hasAdvancedRequest?: boolean; hasAdvancedRequest?: boolean;
@@ -148,7 +144,7 @@ const JellyseerrSeasons: React.FC<{
hasAdvancedRequest, hasAdvancedRequest,
onAdvancedRequest, onAdvancedRequest,
}) => { }) => {
const { jellyseerrApi, requestMedia } = useJellyseerr(); const { seerrApi, requestMedia } = useSeerr();
const [seasonStates, setSeasonStates] = useState<{ [key: number]: boolean }>( const [seasonStates, setSeasonStates] = useState<{ [key: number]: boolean }>(
{}, {},
); );
@@ -181,7 +177,7 @@ const JellyseerrSeasons: React.FC<{
); );
const requestAll = useCallback(() => { const requestAll = useCallback(() => {
if (details && jellyseerrApi) { if (details && seerrApi) {
const body: MediaRequestBody = { const body: MediaRequestBody = {
mediaId: details.id, mediaId: details.id,
mediaType: MediaType.TV, mediaType: MediaType.TV,
@@ -198,7 +194,7 @@ const JellyseerrSeasons: React.FC<{
requestMedia(details.name, body, refetch); requestMedia(details.name, body, refetch);
} }
}, [ }, [
jellyseerrApi, seerrApi,
seasons, seasons,
details, details,
hasAdvancedRequest, hasAdvancedRequest,
@@ -210,15 +206,15 @@ const JellyseerrSeasons: React.FC<{
const promptRequestAll = useCallback( const promptRequestAll = useCallback(
() => () =>
Alert.alert( Alert.alert(
t("jellyseerr.confirm"), t("seerr.confirm"),
t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), t("seerr.are_you_sure_you_want_to_request_all_seasons"),
[ [
{ {
text: t("jellyseerr.cancel"), text: t("seerr.cancel"),
style: "cancel", style: "cancel",
}, },
{ {
text: t("jellyseerr.yes"), text: t("seerr.yes"),
onPress: requestAll, onPress: requestAll,
}, },
], ],
@@ -301,10 +297,10 @@ const JellyseerrSeasons: React.FC<{
<Tags <Tags
textClass='' textClass=''
tags={[ tags={[
t("jellyseerr.season_number", { t("seerr.season_number", {
season_number: season.seasonNumber, season_number: season.seasonNumber,
}), }),
t("jellyseerr.number_episodes", { t("seerr.number_episodes", {
episode_number: season.episodeCount, episode_number: season.episodeCount,
}), }),
]} ]}
@@ -312,7 +308,7 @@ const JellyseerrSeasons: React.FC<{
{[0].map(() => { {[0].map(() => {
const canRequest = season.status === MediaStatus.UNKNOWN; const canRequest = season.status === MediaStatus.UNKNOWN;
return ( return (
<JellyseerrStatusIcon <SeerrStatusIcon
key={0} key={0}
onPress={() => onPress={() =>
requestSeason(canRequest, season.seasonNumber) requestSeason(canRequest, season.seasonNumber)
@@ -326,7 +322,7 @@ const JellyseerrSeasons: React.FC<{
</View> </View>
</TouchableOpacity> </TouchableOpacity>
{seasonStates?.[season.seasonNumber] && ( {seasonStates?.[season.seasonNumber] && (
<JellyseerrSeasonEpisodes <SeerrSeasonEpisodes
key={season.seasonNumber} key={season.seasonNumber}
details={details} details={details}
seasonNumber={season.seasonNumber} seasonNumber={season.seasonNumber}
@@ -338,4 +334,4 @@ const JellyseerrSeasons: React.FC<{
); );
}; };
export default JellyseerrSeasons; export default SeerrSeasons;

View File

@@ -1,181 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { toast } from "sonner-native";
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { Button } from "../Button";
import { Input } from "../common/Input";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
export const JellyseerrSettings = () => {
const { jellyseerrUser, setJellyseerrUser, clearAllJellyseerData } =
useJellyseerr();
const { t } = useTranslation();
const [user] = useAtom(userAtom);
const { settings, updateSettings } = useSettings();
const [jellyseerrPassword, setJellyseerrPassword] = useState<
string | undefined
>(undefined);
const [jellyseerrServerUrl, setjellyseerrServerUrl] = useState<
string | undefined
>(settings?.jellyseerrServerUrl || undefined);
const loginToJellyseerrMutation = useMutation({
mutationFn: async () => {
if (!jellyseerrServerUrl && !settings?.jellyseerrServerUrl)
throw new Error("Missing server url");
if (!user?.Name)
throw new Error("Missing required information for login");
const jellyseerrTempApi = new JellyseerrApi(
jellyseerrServerUrl || settings.jellyseerrServerUrl || "",
);
const testResult = await jellyseerrTempApi.test();
if (!testResult.isValid) throw new Error("Invalid server url");
return jellyseerrTempApi.login(user.Name, jellyseerrPassword || "");
},
onSuccess: (user) => {
setJellyseerrUser(user);
updateSettings({ jellyseerrServerUrl });
},
onError: () => {
toast.error(t("jellyseerr.failed_to_login"));
},
onSettled: () => {
setJellyseerrPassword(undefined);
},
});
const clearData = () => {
clearAllJellyseerData().finally(() => {
setJellyseerrUser(undefined);
setJellyseerrPassword(undefined);
setjellyseerrServerUrl(undefined);
});
};
return (
<View className=''>
<View>
{jellyseerrUser ? (
<>
<ListGroup title={"Jellyseerr"}>
<ListItem
title={t(
"home.settings.plugins.jellyseerr.total_media_requests",
)}
value={jellyseerrUser?.requestCount?.toString()}
/>
<ListItem
title={t("home.settings.plugins.jellyseerr.movie_quota_limit")}
value={
jellyseerrUser?.movieQuotaLimit?.toString() ??
t("home.settings.plugins.jellyseerr.unlimited")
}
/>
<ListItem
title={t("home.settings.plugins.jellyseerr.movie_quota_days")}
value={
jellyseerrUser?.movieQuotaDays?.toString() ??
t("home.settings.plugins.jellyseerr.unlimited")
}
/>
<ListItem
title={t("home.settings.plugins.jellyseerr.tv_quota_limit")}
value={
jellyseerrUser?.tvQuotaLimit?.toString() ??
t("home.settings.plugins.jellyseerr.unlimited")
}
/>
<ListItem
title={t("home.settings.plugins.jellyseerr.tv_quota_days")}
value={
jellyseerrUser?.tvQuotaDays?.toString() ??
t("home.settings.plugins.jellyseerr.unlimited")
}
/>
</ListGroup>
<View className='p-4'>
<Button color='red' onPress={clearData}>
{t(
"home.settings.plugins.jellyseerr.reset_jellyseerr_config_button",
)}
</Button>
</View>
</>
) : (
<View className='flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900'>
<Text className='text-xs text-red-600 mb-2'>
{t("home.settings.plugins.jellyseerr.jellyseerr_warning")}
</Text>
<Text className='font-bold mb-1'>
{t("home.settings.plugins.jellyseerr.server_url")}
</Text>
<View className='flex flex-col shrink mb-2'>
<Text className='text-xs text-gray-600'>
{t("home.settings.plugins.jellyseerr.server_url_hint")}
</Text>
</View>
<Input
className='border border-neutral-800 mb-2'
placeholder={t(
"home.settings.plugins.jellyseerr.server_url_placeholder",
)}
value={jellyseerrServerUrl ?? settings?.jellyseerrServerUrl}
defaultValue={
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
}
keyboardType='url'
returnKeyType='done'
autoCapitalize='none'
textContentType='URL'
onChangeText={setjellyseerrServerUrl}
editable={!loginToJellyseerrMutation.isPending}
/>
<View>
<Text className='font-bold mb-2'>
{t("home.settings.plugins.jellyseerr.password")}
</Text>
<Input
className='border border-neutral-800'
autoFocus={true}
focusable={true}
placeholder={t(
"home.settings.plugins.jellyseerr.password_placeholder",
{ username: user?.Name },
)}
value={jellyseerrPassword}
keyboardType='default'
secureTextEntry={true}
returnKeyType='done'
autoCapitalize='none'
textContentType='password'
onChangeText={setJellyseerrPassword}
editable={!loginToJellyseerrMutation.isPending}
/>
<Button
loading={loginToJellyseerrMutation.isPending}
disabled={loginToJellyseerrMutation.isPending}
color='purple'
className='h-12 mt-2'
onPress={() => loginToJellyseerrMutation.mutate()}
>
{t("home.settings.plugins.jellyseerr.login_button")}
</Button>
</View>
</View>
)}
</View>
</View>
);
};

View File

@@ -19,7 +19,7 @@ export const PluginSettings = () => {
className='mb-4' className='mb-4'
> >
<ListItem <ListItem
onPress={() => router.push("/settings/plugins/jellyseerr/page")} onPress={() => router.push("/settings/plugins/seerr/page")}
title={"Seerr"} title={"Seerr"}
showArrow showArrow
/> />

View File

@@ -0,0 +1,174 @@
import { useMutation } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { toast } from "sonner-native";
import { SeerrApi, useSeerr } from "@/hooks/useSeerr";
import { userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { Button } from "../Button";
import { Input } from "../common/Input";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
export const SeerrSettings = () => {
const { seerrUser, setSeerrUser, clearAllSeerrData } = useSeerr();
const { t } = useTranslation();
const [user] = useAtom(userAtom);
const { settings, updateSettings } = useSettings();
const [seerrPassword, setSeerrPassword] = useState<string | undefined>(
undefined,
);
const [seerrServerUrl, setSeerrServerUrl] = useState<string | undefined>(
settings?.seerrServerUrl || undefined,
);
const loginToSeerrMutation = useMutation({
mutationFn: async () => {
if (!seerrServerUrl && !settings?.seerrServerUrl)
throw new Error("Missing server url");
if (!user?.Name)
throw new Error("Missing required information for login");
const seerrTempApi = new SeerrApi(
seerrServerUrl || settings.seerrServerUrl || "",
);
const testResult = await seerrTempApi.test();
if (!testResult.isValid) throw new Error("Invalid server url");
return seerrTempApi.login(user.Name, seerrPassword || "");
},
onSuccess: (user) => {
setSeerrUser(user);
updateSettings({ seerrServerUrl });
},
onError: () => {
toast.error(t("seerr.failed_to_login"));
},
onSettled: () => {
setSeerrPassword(undefined);
},
});
const clearData = () => {
clearAllSeerrData().finally(() => {
setSeerrUser(undefined);
setSeerrPassword(undefined);
setSeerrServerUrl(undefined);
});
};
return (
<View className=''>
<View>
{seerrUser ? (
<>
<ListGroup title={"Seerr"}>
<ListItem
title={t("home.settings.plugins.seerr.total_media_requests")}
value={seerrUser?.requestCount?.toString()}
/>
<ListItem
title={t("home.settings.plugins.seerr.movie_quota_limit")}
value={
seerrUser?.movieQuotaLimit?.toString() ??
t("home.settings.plugins.seerr.unlimited")
}
/>
<ListItem
title={t("home.settings.plugins.seerr.movie_quota_days")}
value={
seerrUser?.movieQuotaDays?.toString() ??
t("home.settings.plugins.seerr.unlimited")
}
/>
<ListItem
title={t("home.settings.plugins.seerr.tv_quota_limit")}
value={
seerrUser?.tvQuotaLimit?.toString() ??
t("home.settings.plugins.seerr.unlimited")
}
/>
<ListItem
title={t("home.settings.plugins.seerr.tv_quota_days")}
value={
seerrUser?.tvQuotaDays?.toString() ??
t("home.settings.plugins.seerr.unlimited")
}
/>
</ListGroup>
<View className='p-4'>
<Button color='red' onPress={clearData}>
{t("home.settings.plugins.seerr.reset_seerr_config_button")}
</Button>
</View>
</>
) : (
<View className='flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900'>
<Text className='text-xs text-red-600 mb-2'>
{t("home.settings.plugins.seerr.seerr_warning")}
</Text>
<Text className='font-bold mb-1'>
{t("home.settings.plugins.seerr.server_url")}
</Text>
<View className='flex flex-col shrink mb-2'>
<Text className='text-xs text-gray-600'>
{t("home.settings.plugins.seerr.server_url_hint")}
</Text>
</View>
<Input
className='border border-neutral-800 mb-2'
placeholder={t(
"home.settings.plugins.seerr.server_url_placeholder",
)}
value={seerrServerUrl ?? settings?.seerrServerUrl}
defaultValue={settings?.seerrServerUrl ?? seerrServerUrl}
keyboardType='url'
returnKeyType='done'
autoCapitalize='none'
textContentType='URL'
onChangeText={setSeerrServerUrl}
editable={!loginToSeerrMutation.isPending}
/>
<View>
<Text className='font-bold mb-2'>
{t("home.settings.plugins.seerr.password")}
</Text>
<Input
className='border border-neutral-800'
autoFocus={true}
focusable={true}
placeholder={t(
"home.settings.plugins.seerr.password_placeholder",
{ username: user?.Name },
)}
value={seerrPassword}
keyboardType='default'
secureTextEntry={true}
returnKeyType='done'
autoCapitalize='none'
textContentType='password'
onChangeText={setSeerrPassword}
editable={!loginToSeerrMutation.isPending}
/>
<Button
loading={loginToSeerrMutation.isPending}
disabled={loginToSeerrMutation.isPending}
color='purple'
className='h-12 mt-2'
onPress={() => loginToSeerrMutation.mutate()}
>
{t("home.settings.plugins.seerr.login_button")}
</Button>
</View>
</View>
)}
</View>
</View>
);
};

View File

@@ -2,7 +2,7 @@ import axios, { type AxiosError, type AxiosInstance } from "axios";
import { atom } from "jotai"; import { atom } from "jotai";
import { useAtom } from "jotai/index"; import { useAtom } from "jotai/index";
import { inRange } from "lodash"; import { inRange } from "lodash";
import type { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; import type { User as SeerrUser } from "@/utils/jellyseerr/server/entity/User";
import type { import type {
MovieResult, MovieResult,
Results, Results,
@@ -62,12 +62,12 @@ interface SearchResults {
results: Results[]; results: Results[];
} }
const JELLYSEERR_USER = "JELLYSEERR_USER"; const SEERR_USER = "SEERR_USER";
const JELLYSEERR_COOKIES = "JELLYSEERR_COOKIES"; const SEERR_COOKIES = "SEERR_COOKIES";
export const clearJellyseerrStorageData = () => { export const clearSeerrStorageData = () => {
storage.remove(JELLYSEERR_USER); storage.remove(SEERR_USER);
storage.remove(JELLYSEERR_COOKIES); storage.remove(SEERR_COOKIES);
}; };
export enum Endpoints { export enum Endpoints {
@@ -111,7 +111,7 @@ export type TestResult =
isValid: false; isValid: false;
}; };
export class JellyseerrApi { export class SeerrApi {
axios: AxiosInstance; axios: AxiosInstance;
constructor(baseUrl: string) { constructor(baseUrl: string) {
@@ -126,8 +126,8 @@ export class JellyseerrApi {
} }
async test(): Promise<TestResult> { async test(): Promise<TestResult> {
const user = storage.get<JellyseerrUser>(JELLYSEERR_USER); const user = storage.get<SeerrUser>(SEERR_USER);
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES); const cookies = storage.get<string[]>(SEERR_COOKIES);
if (user && cookies) { if (user && cookies) {
return Promise.resolve({ return Promise.resolve({
@@ -142,15 +142,13 @@ export class JellyseerrApi {
const { status, headers, data } = response; const { status, headers, data } = response;
if (inRange(status, 200, 299)) { if (inRange(status, 200, 299)) {
if (data.version < "2.0.0") { if (data.version < "2.0.0") {
const error = t( const error = t("seerr.toasts.seer_does_not_meet_requirements");
"jellyseerr.toasts.jellyseer_does_not_meet_requirements",
);
toast.error(error); toast.error(error);
throw Error(error); throw Error(error);
} }
storage.setAny( storage.setAny(
JELLYSEERR_COOKIES, SEERR_COOKIES,
headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [], headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [],
); );
return { return {
@@ -158,7 +156,7 @@ export class JellyseerrApi {
requiresPass: true, requiresPass: true,
}; };
} }
toast.error(t("jellyseerr.toasts.jellyseerr_test_failed")); toast.error(t("seerr.toasts.seerr_test_failed"));
writeErrorLog( writeErrorLog(
`Seerr returned a ${status} for url:\n${response.config.url}`, `Seerr returned a ${status} for url:\n${response.config.url}`,
response.data, response.data,
@@ -169,7 +167,7 @@ export class JellyseerrApi {
}; };
}) })
.catch((e) => { .catch((e) => {
const msg = t("jellyseerr.toasts.failed_to_test_jellyseerr_server_url"); const msg = t("seerr.toasts.failed_to_test_seerr_server_url");
toast.error(msg); toast.error(msg);
console.error(msg, e); console.error(msg, e);
return { return {
@@ -179,9 +177,9 @@ export class JellyseerrApi {
}); });
} }
async login(username: string, password: string): Promise<JellyseerrUser> { async login(username: string, password: string): Promise<SeerrUser> {
return this.axios return this.axios
?.post<JellyseerrUser>(Endpoints.API_V1 + Endpoints.AUTH_JELLYFIN, { ?.post<SeerrUser>(Endpoints.API_V1 + Endpoints.AUTH_JELLYFIN, {
username, username,
password, password,
email: username, email: username,
@@ -189,7 +187,7 @@ export class JellyseerrApi {
.then((response) => { .then((response) => {
const user = response?.data; const user = response?.data;
if (!user) throw Error("Login failed"); if (!user) throw Error("Login failed");
storage.setAny(JELLYSEERR_USER, user); storage.setAny(SEERR_USER, user);
return user; return user;
}); });
} }
@@ -364,7 +362,7 @@ export class JellyseerrApi {
const issue = response.data; const issue = response.data;
if (issue.status === IssueStatus.OPEN) { if (issue.status === IssueStatus.OPEN) {
toast.success(t("jellyseerr.toasts.issue_submitted")); toast.success(t("seerr.toasts.issue_submitted"));
} }
return issue; return issue;
}); });
@@ -392,7 +390,7 @@ export class JellyseerrApi {
const cookies = response.headers["set-cookie"]; const cookies = response.headers["set-cookie"];
if (cookies) { if (cookies) {
storage.setAny( storage.setAny(
JELLYSEERR_COOKIES, SEERR_COOKIES,
response.headers["set-cookie"]?.flatMap((c) => c.split("; ")), response.headers["set-cookie"]?.flatMap((c) => c.split("; ")),
); );
} }
@@ -404,7 +402,7 @@ export class JellyseerrApi {
error.response?.data, error.response?.data,
); );
if (error.response?.status === 403) { if (error.response?.status === 403) {
clearJellyseerrStorageData(); clearSeerrStorageData();
} }
return Promise.reject(error); return Promise.reject(error);
}, },
@@ -412,7 +410,7 @@ export class JellyseerrApi {
this.axios.interceptors.request.use( this.axios.interceptors.request.use(
async (config) => { async (config) => {
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES); const cookies = storage.get<string[]>(SEERR_COOKIES);
if (cookies) { if (cookies) {
const headerName = this.axios.defaults.xsrfHeaderName!; const headerName = this.axios.defaults.xsrfHeaderName!;
const xsrfToken = cookies const xsrfToken = cookies
@@ -431,59 +429,55 @@ export class JellyseerrApi {
} }
} }
const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER)); const seerrUserAtom = atom(storage.get<SeerrUser>(SEERR_USER));
export const useJellyseerr = () => { export const useSeerr = () => {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom); const [seerrUser, setSeerrUser] = useAtom(seerrUserAtom);
const queryClient = useNetworkAwareQueryClient(); const queryClient = useNetworkAwareQueryClient();
const jellyseerrApi = useMemo(() => { const seerrApi = useMemo(() => {
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES); const cookies = storage.get<string[]>(SEERR_COOKIES);
if (settings?.jellyseerrServerUrl && cookies && jellyseerrUser) { if (settings?.seerrServerUrl && cookies && seerrUser) {
return new JellyseerrApi(settings?.jellyseerrServerUrl); return new SeerrApi(settings?.seerrServerUrl);
} }
return undefined; return undefined;
}, [settings?.jellyseerrServerUrl, jellyseerrUser]); }, [settings?.seerrServerUrl, seerrUser]);
const clearAllJellyseerData = useCallback(async () => { const clearAllSeerrData = useCallback(async () => {
clearJellyseerrStorageData(); clearSeerrStorageData();
setJellyseerrUser(undefined); setSeerrUser(undefined);
updateSettings({ jellyseerrServerUrl: undefined }); updateSettings({ seerrServerUrl: undefined });
}, []); }, []);
const requestMedia = useCallback( const requestMedia = useCallback(
(title: string, request: MediaRequestBody, onSuccess?: () => void) => { (title: string, request: MediaRequestBody, onSuccess?: () => void) => {
jellyseerrApi?.request?.(request)?.then(async (mediaRequest) => { seerrApi?.request?.(request)?.then(async (mediaRequest) => {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["search", "jellyseerr"], queryKey: ["search", "seerr"],
}); });
switch (mediaRequest.status) { switch (mediaRequest.status) {
case MediaRequestStatus.PENDING: case MediaRequestStatus.PENDING:
case MediaRequestStatus.APPROVED: case MediaRequestStatus.APPROVED:
toast.success( toast.success(t("seerr.toasts.requested_item", { item: title }));
t("jellyseerr.toasts.requested_item", { item: title }),
);
onSuccess?.(); onSuccess?.();
break; break;
case MediaRequestStatus.DECLINED: case MediaRequestStatus.DECLINED:
toast.error( toast.error(t("seerr.toasts.you_dont_have_permission_to_request"));
t("jellyseerr.toasts.you_dont_have_permission_to_request"),
);
break; break;
case MediaRequestStatus.FAILED: case MediaRequestStatus.FAILED:
toast.error( toast.error(
t("jellyseerr.toasts.something_went_wrong_requesting_media"), t("seerr.toasts.something_went_wrong_requesting_media"),
); );
break; break;
} }
}); });
}, },
[jellyseerrApi], [seerrApi],
); );
const isJellyseerrMovieOrTvResult = ( const isSeerrMovieOrTvResult = (
items: any | null | undefined, items: any | null | undefined,
): items is MovieResult | TvResult => { ): items is MovieResult | TvResult => {
return ( return (
@@ -496,7 +490,7 @@ export const useJellyseerr = () => {
const getTitle = ( const getTitle = (
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
) => { ) => {
return isJellyseerrMovieOrTvResult(item) return isSeerrMovieOrTvResult(item)
? item.mediaType === MediaType.MOVIE ? item.mediaType === MediaType.MOVIE
? item?.title ? item?.title
: item?.name : item?.name
@@ -509,7 +503,7 @@ export const useJellyseerr = () => {
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
) => { ) => {
return new Date( return new Date(
(isJellyseerrMovieOrTvResult(item) (isSeerrMovieOrTvResult(item)
? item.mediaType === MediaType.MOVIE ? item.mediaType === MediaType.MOVIE
? item?.releaseDate ? item?.releaseDate
: item?.firstAirDate : item?.firstAirDate
@@ -522,32 +516,32 @@ export const useJellyseerr = () => {
const getMediaType = ( const getMediaType = (
item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast,
): MediaType => { ): MediaType => {
return isJellyseerrMovieOrTvResult(item) return isSeerrMovieOrTvResult(item)
? (item.mediaType as MediaType) ? (item.mediaType as MediaType)
: item?.mediaInfo?.mediaType; : item?.mediaInfo?.mediaType;
}; };
const jellyseerrRegion = useMemo( const seerrRegion = useMemo(
// streamingRegion and discoverRegion exists. region doesn't // streamingRegion and discoverRegion exists. region doesn't
() => jellyseerrUser?.settings?.discoverRegion || "US", () => seerrUser?.settings?.discoverRegion || "US",
[jellyseerrUser], [seerrUser],
); );
const jellyseerrLocale = useMemo(() => { const seerrLocale = useMemo(() => {
return jellyseerrUser?.settings?.locale || "en"; return seerrUser?.settings?.locale || "en";
}, [jellyseerrUser]); }, [seerrUser]);
return { return {
jellyseerrApi, seerrApi,
jellyseerrUser, seerrUser,
setJellyseerrUser, setSeerrUser,
clearAllJellyseerData, clearAllSeerrData,
isJellyseerrMovieOrTvResult, isSeerrMovieOrTvResult,
getTitle, getTitle,
getYear, getYear,
getMediaType, getMediaType,
jellyseerrRegion, seerrRegion,
jellyseerrLocale, seerrLocale,
requestMedia, requestMedia,
}; };
}; };

View File

@@ -23,7 +23,7 @@ import { getDeviceName } from "react-native-device-info";
import uuid from "react-native-uuid"; import uuid from "react-native-uuid";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { useInterval } from "@/hooks/useInterval"; import { useInterval } from "@/hooks/useInterval";
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; import { SeerrApi, useSeerr } from "@/hooks/useSeerr";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { writeErrorLog, writeInfoLog } from "@/utils/log"; import { writeErrorLog, writeInfoLog } from "@/utils/log";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
@@ -113,7 +113,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
const [isPolling, setIsPolling] = useState<boolean>(false); const [isPolling, setIsPolling] = useState<boolean>(false);
const [secret, setSecret] = useState<string | null>(null); const [secret, setSecret] = useState<string | null>(null);
const { setPluginSettings, refreshStreamyfinPluginSettings } = useSettings(); const { setPluginSettings, refreshStreamyfinPluginSettings } = useSettings();
const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(); const { clearAllSeerrData, setSeerrUser } = useSeerr();
const headers = useMemo(() => { const headers = useMemo(() => {
if (!deviceId) return {}; if (!deviceId) return {};
@@ -290,13 +290,13 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
} }
const recentPluginSettings = await refreshStreamyfinPluginSettings(); const recentPluginSettings = await refreshStreamyfinPluginSettings();
if (recentPluginSettings?.jellyseerrServerUrl?.value) { if (recentPluginSettings?.seerrServerUrl?.value) {
const jellyseerrApi = new JellyseerrApi( const seerrApi = new SeerrApi(
recentPluginSettings.jellyseerrServerUrl.value, recentPluginSettings.seerrServerUrl.value,
); );
await jellyseerrApi.test().then((result) => { await seerrApi.test().then((result) => {
if (result.isValid && result.requiresPass) { if (result.isValid && result.requiresPass) {
jellyseerrApi.login(username, password).then(setJellyseerrUser); seerrApi.login(username, password).then(setSeerrUser);
} }
}); });
} }
@@ -349,7 +349,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setUser(null); setUser(null);
setApi(null); setApi(null);
setPluginSettings(undefined); setPluginSettings(undefined);
await clearAllJellyseerData(); await clearAllSeerrData();
// Note: We keep saved credentials for quick switching back // Note: We keep saved credentials for quick switching back
}, },
onError: (error) => { onError: (error) => {

View File

@@ -96,7 +96,7 @@
"a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin", "a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin",
"features_title": "Features", "features_title": "Features",
"features_description": "Streamyfin offers many features and integrates with a wide array of software which you can find in the settings menu, including:", "features_description": "Streamyfin offers many features and integrates with a wide array of software which you can find in the settings menu, including:",
"jellyseerr_feature_description": "Connect to your Seerr instance and request movies directly in the app.", "seerr_feature_description": "Connect to your Seerr instance and request movies directly in the app.",
"downloads_feature_title": "Downloads", "downloads_feature_title": "Downloads",
"downloads_feature_description": "Download movies and TV shows to view offline.", "downloads_feature_description": "Download movies and TV shows to view offline.",
"chromecast_feature_description": "Cast movies and TV shows to your Chromecast devices.", "chromecast_feature_description": "Cast movies and TV shows to your Chromecast devices.",
@@ -326,8 +326,8 @@
}, },
"plugins": { "plugins": {
"plugins_title": "Plugins", "plugins_title": "Plugins",
"jellyseerr": { "seerr": {
"jellyseerr_warning": "This integration is in early development. Features may change.", "seerr_warning": "This integration is in early development. Features may change.",
"server_url": "Server URL", "server_url": "Server URL",
"server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)",
"server_url_placeholder": "Seerr URL", "server_url_placeholder": "Seerr URL",
@@ -339,7 +339,7 @@
"movie_quota_days": "Movie Quota Days", "movie_quota_days": "Movie Quota Days",
"tv_quota_limit": "TV Quota Limit", "tv_quota_limit": "TV Quota Limit",
"tv_quota_days": "TV Quota Days", "tv_quota_days": "TV Quota Days",
"reset_jellyseerr_config_button": "Reset Seerr Config", "reset_seerr_config_button": "Reset Seerr Config",
"unlimited": "Unlimited", "unlimited": "Unlimited",
"plus_n_more": "+{{n}} more", "plus_n_more": "+{{n}} more",
"order_by": { "order_by": {
@@ -675,7 +675,7 @@
"for_kids": "For Kids", "for_kids": "For Kids",
"news": "News" "news": "News"
}, },
"jellyseerr": { "seerr": {
"confirm": "Confirm", "confirm": "Confirm",
"cancel": "Cancel", "cancel": "Cancel",
"yes": "Yes", "yes": "Yes",
@@ -719,9 +719,9 @@
"requested_by": "Requested by {{user}}", "requested_by": "Requested by {{user}}",
"unknown_user": "Unknown User", "unknown_user": "Unknown User",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", "seer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0",
"jellyseerr_test_failed": "Seerr test failed. Please try again.", "seerr_test_failed": "Seerr test failed. Please try again.",
"failed_to_test_jellyseerr_server_url": "Failed to test Seerr server URL", "failed_to_test_seerr_server_url": "Failed to test Seerr server URL",
"issue_submitted": "Issue Submitted!", "issue_submitted": "Issue Submitted!",
"requested_item": "Requested {{item}}!", "requested_item": "Requested {{item}}!",
"you_dont_have_permission_to_request": "You don't have permission to request!", "you_dont_have_permission_to_request": "You don't have permission to request!",

View File

@@ -1,5 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useSeerr } from "@/hooks/useSeerr";
import { import {
MediaRequestStatus, MediaRequestStatus,
MediaStatus, MediaStatus,
@@ -18,13 +18,13 @@ import type MediaRequest from "../jellyseerr/server/entity/MediaRequest";
import type { MovieDetails } from "../jellyseerr/server/models/Movie"; import type { MovieDetails } from "../jellyseerr/server/models/Movie";
import type { TvDetails } from "../jellyseerr/server/models/Tv"; import type { TvDetails } from "../jellyseerr/server/models/Tv";
export const useJellyseerrCanRequest = ( export const useSeerrCanRequest = (
item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast, item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast,
) => { ) => {
const { jellyseerrUser } = useJellyseerr(); const { seerrUser } = useSeerr();
const canRequest = useMemo(() => { const canRequest = useMemo(() => {
if (!jellyseerrUser || !item) return false; if (!seerrUser || !item) return false;
const canNotRequest = const canNotRequest =
item?.mediaInfo?.requests?.some( item?.mediaInfo?.requests?.some(
@@ -46,22 +46,22 @@ export const useJellyseerrCanRequest = (
? Permission.REQUEST_MOVIE ? Permission.REQUEST_MOVIE
: Permission.REQUEST_TV, : Permission.REQUEST_TV,
], ],
jellyseerrUser.permissions, seerrUser.permissions,
{ type: "or" }, { type: "or" },
); );
return userHasPermission && !canNotRequest; return userHasPermission && !canNotRequest;
}, [item, jellyseerrUser]); }, [item, seerrUser]);
const hasAdvancedRequestPermission = useMemo(() => { const hasAdvancedRequestPermission = useMemo(() => {
if (!jellyseerrUser) return false; if (!seerrUser) return false;
return hasPermission( return hasPermission(
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
jellyseerrUser.permissions, seerrUser.permissions,
{ type: "or" }, { type: "or" },
); );
}, [jellyseerrUser]); }, [seerrUser]);
return [canRequest, hasAdvancedRequestPermission]; return [canRequest, hasAdvancedRequestPermission];
}; };

View File

@@ -174,7 +174,7 @@ export type Settings = {
disableHapticFeedback: boolean; disableHapticFeedback: boolean;
subtitleSize: number; subtitleSize: number;
safeAreaInControlsEnabled: boolean; safeAreaInControlsEnabled: boolean;
jellyseerrServerUrl?: string; seerrServerUrl?: string;
useKefinTweaks: boolean; useKefinTweaks: boolean;
hiddenLibraries?: string[]; hiddenLibraries?: string[];
enableH265ForChromecast: boolean; enableH265ForChromecast: boolean;
@@ -259,7 +259,7 @@ export const defaultValues: Settings = {
disableHapticFeedback: false, disableHapticFeedback: false,
subtitleSize: 100, // Scale value * 100, so 100 = 1.0x subtitleSize: 100, // Scale value * 100, so 100 = 1.0x
safeAreaInControlsEnabled: true, safeAreaInControlsEnabled: true,
jellyseerrServerUrl: undefined, seerrServerUrl: undefined,
useKefinTweaks: false, useKefinTweaks: false,
hiddenLibraries: [], hiddenLibraries: [],
enableH265ForChromecast: false, enableH265ForChromecast: false,