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

@@ -89,16 +89,16 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
</Text>
<View className='flex flex-row items-center mt-4'>
<Image
source={require("@/assets/icons/jellyseerr-logo.svg")}
source={require("@/assets/icons/seerr-logo.svg")}
style={{
width: 50,
height: 50,
}}
/>
<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'>
{t("home.intro.jellyseerr_feature_description")}
{t("home.intro.seerr_feature_description")}
</Text>
</View>
</View>

View File

@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useMemo } from "react";
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 type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
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 }) => {
const { jellyseerrApi, getMediaType } = useJellyseerr();
const { seerrApi, getMediaType } = useSeerr();
const mediaType = useMemo(() => getMediaType(result), [result]);
const { data, isLoading } = useQuery({
queryKey: ["jellyseerr", result.id, mediaType, "ratings"],
queryKey: ["seerr", result.id, mediaType, "ratings"],
queryFn: async () => {
return mediaType === MediaType.MOVIE
? jellyseerrApi?.movieRatings(result.id)
: jellyseerrApi?.tvRatings(result.id);
? seerrApi?.movieRatings(result.id)
: seerrApi?.tvRatings(result.id);
},
staleTime: (5).minutesToMilliseconds(),
retry: false,
enabled: !!jellyseerrApi,
enabled: !!seerrApi,
});
return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,15 +7,15 @@ import Animated, {
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter";
import { TouchableSeerrRouter } from "@/components/common/SeerrItemRouter";
import { Text } from "@/components/common/Text";
import { Tag, Tags } from "@/components/GenreTags";
import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard";
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon";
import { textShadowStyle } from "@/components/seerr/discover/GenericSlideCard";
import SeerrMediaIcon from "@/components/seerr/SeerrMediaIcon";
import SeerrStatusIcon from "@/components/seerr/SeerrStatusIcon";
import { Colors } from "@/constants/Colors";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest";
import { useSeerr } from "@/hooks/useSeerr";
import { useSeerrCanRequest } from "@/utils/_seerr/useSeerrCanRequest";
import { MediaStatus } from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { DownloadingItem } from "@/utils/jellyseerr/server/lib/downloadtracker";
@@ -34,13 +34,13 @@ interface Props extends ViewProps {
mediaRequest?: MediaRequest;
}
const JellyseerrPoster: React.FC<Props> = ({
const SeerrPoster: React.FC<Props> = ({
item,
horizontal,
showDownloadInfo,
mediaRequest,
}) => {
const { jellyseerrApi, getTitle, getYear, getMediaType } = useJellyseerr();
const { seerrApi, getTitle, getYear, getMediaType } = useSeerr();
const loadingOpacity = useSharedValue(1);
const imageOpacity = useSharedValue(0);
const { t } = useTranslation();
@@ -56,16 +56,13 @@ const JellyseerrPoster: React.FC<Props> = ({
const backdropSrc = useMemo(
() =>
jellyseerrApi?.imageProxy(
item?.backdropPath,
"w1920_and_h800_multi_faces",
),
[item, jellyseerrApi, horizontal],
seerrApi?.imageProxy(item?.backdropPath, "w1920_and_h800_multi_faces"),
[item, seerrApi, horizontal],
);
const posterSrc = useMemo(
() => jellyseerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"),
[item, jellyseerrApi, horizontal],
() => seerrApi?.imageProxy(item?.posterPath, "w300_and_h450_face"),
[item, seerrApi, horizontal],
);
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 ratio = useMemo(() => (horizontal ? "15/10" : "10/15"), [horizontal]);
const [canRequest] = useJellyseerrCanRequest(item);
const [canRequest] = useSeerrCanRequest(item);
const is4k = useMemo(() => mediaRequest?.is4k === true, [mediaRequest]);
@@ -109,7 +106,7 @@ const JellyseerrPoster: React.FC<Props> = ({
second,
third,
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;
@@ -121,7 +118,7 @@ const JellyseerrPoster: React.FC<Props> = ({
}, [mediaRequest, is4k]);
return (
<TouchableJellyseerrRouter
<TouchableSeerrRouter
result={item}
mediaTitle={title}
releaseYear={releaseYear}
@@ -184,12 +181,12 @@ const JellyseerrPoster: React.FC<Props> = ({
)}
</>
)}
<JellyseerrStatusIcon
<SeerrStatusIcon
className='absolute bottom-1 right-1'
showRequestIcon={canRequest}
mediaStatus={mediaRequest?.media?.status || item?.mediaInfo?.status}
/>
<JellyseerrMediaIcon
<SeerrMediaIcon
className='absolute top-1 left-1'
mediaType={mediaType}
/>
@@ -201,8 +198,8 @@ const JellyseerrPoster: React.FC<Props> = ({
{releaseYear || ""}
</Text>
</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 { Platform, View } from "react-native";
import { FilterButton } from "@/components/filters/FilterButton";
import { JellyseerrSearchSort } from "@/components/jellyseerr/JellyseerrIndexPage";
import { SeerrSearchSort } from "@/components/seerr/SeerrIndexPage";
interface DiscoverFiltersProps {
searchFilterId: string;
orderFilterId: string;
jellyseerrOrderBy: JellyseerrSearchSort;
setJellyseerrOrderBy: (value: JellyseerrSearchSort) => void;
jellyseerrSortOrder: "asc" | "desc";
setJellyseerrSortOrder: (value: "asc" | "desc") => void;
seerrOrderBy: SeerrSearchSort;
setSeerrOrderBy: (value: SeerrSearchSort) => void;
seerrSortOrder: "asc" | "desc";
setSeerrSortOrder: (value: "asc" | "desc") => void;
t: (key: string) => string;
}
const sortOptions = Object.keys(JellyseerrSearchSort).filter((v) =>
const sortOptions = Object.keys(SeerrSearchSort).filter((v) =>
Number.isNaN(Number(v)),
);
@@ -22,10 +22,10 @@ const orderOptions = ["asc", "desc"] as const;
export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
searchFilterId,
orderFilterId,
jellyseerrOrderBy,
setJellyseerrOrderBy,
jellyseerrSortOrder,
setJellyseerrSortOrder,
seerrOrderBy,
setSeerrOrderBy,
seerrSortOrder,
setSeerrSortOrder,
t,
}) => {
if (Platform.OS === "ios") {
@@ -52,16 +52,16 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
<Picker
label={t("library.filters.sort_by")}
options={sortOptions.map((item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`),
t(`home.settings.plugins.seerr.order_by.${item}`),
)}
variant='menu'
selectedIndex={sortOptions.indexOf(
jellyseerrOrderBy as unknown as string,
seerrOrderBy as unknown as string,
)}
onOptionSelected={(event: any) => {
const index = event.nativeEvent.index;
setJellyseerrOrderBy(
sortOptions[index] as unknown as JellyseerrSearchSort,
setSeerrOrderBy(
sortOptions[index] as unknown as SeerrSearchSort,
);
}}
/>
@@ -69,10 +69,10 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
label={t("library.filters.sort_order")}
options={orderOptions.map((item) => t(`library.filters.${item}`))}
variant='menu'
selectedIndex={orderOptions.indexOf(jellyseerrSortOrder)}
selectedIndex={orderOptions.indexOf(seerrSortOrder)}
onOptionSelected={(event: any) => {
const index = event.nativeEvent.index;
setJellyseerrSortOrder(orderOptions[index]);
setSeerrSortOrder(orderOptions[index]);
}}
/>
</ContextMenu.Items>
@@ -86,17 +86,15 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
<View className='flex flex-row justify-end items-center space-x-1'>
<FilterButton
id={searchFilterId}
queryKey='jellyseerr_search'
queryKey='seerr_search'
queryFn={async () =>
Object.keys(JellyseerrSearchSort).filter((v) =>
Number.isNaN(Number(v)),
)
Object.keys(SeerrSearchSort).filter((v) => Number.isNaN(Number(v)))
}
set={(value) => setJellyseerrOrderBy(value[0])}
values={[jellyseerrOrderBy]}
set={(value) => setSeerrOrderBy(value[0])}
values={[seerrOrderBy]}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`)
t(`home.settings.plugins.seerr.order_by.${item}`)
}
disableSearch={true}
/>
@@ -104,8 +102,8 @@ export const DiscoverFilters: React.FC<DiscoverFiltersProps> = ({
id={orderFilterId}
queryKey='jellysearr_search'
queryFn={async () => ["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])}
values={[jellyseerrSortOrder]}
set={(value) => setSeerrSortOrder(value[0])}
values={[seerrSortOrder]}
title={t("library.filters.sort_order")}
renderItemLabel={(item) => t(`library.filters.${item}`)}
disableSearch={true}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,11 @@ import { useMemo } from "react";
import { View, type ViewProps } from "react-native";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
const JellyseerrMediaIcon: React.FC<
{ mediaType: "tv" | "movie" } & ViewProps
> = ({ mediaType, className, ...props }) => {
const SeerrMediaIcon: React.FC<{ mediaType: "tv" | "movie" } & ViewProps> = ({
mediaType,
className,
...props
}) => {
const style = useMemo(
() =>
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;
}
const JellyseerrStatusIcon: React.FC<Props & ViewProps> = ({
const SeerrStatusIcon: React.FC<Props & ViewProps> = ({
mediaStatus,
showRequestIcon,
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 { useCallback } from "react";
import { TouchableOpacity, type ViewProps } from "react-native";
import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import GenericSlideCard from "@/components/seerr/discover/GenericSlideCard";
import Slide, { type SlideProps } from "@/components/seerr/discover/Slide";
import useRouter from "@/hooks/useAppRouter";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { useSeerr } from "@/hooks/useSeerr";
import {
COMPANY_LOGO_IMAGE_FILTER,
type Network,
@@ -16,14 +16,14 @@ const CompanySlide: React.FC<
{ data: Network[] | Studio[] } & SlideProps & ViewProps
> = ({ slide, data, ...props }) => {
const segments = useSegments();
const { jellyseerrApi } = useJellyseerr();
const { seerrApi } = useSeerr();
const router = useRouter();
const from = (segments as string[])[2] || "(home)";
const navigate = useCallback(
({ id, image, name }: Network | Studio) =>
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 },
}),
[slide],
@@ -40,10 +40,7 @@ const CompanySlide: React.FC<
<GenericSlideCard
className='w-28 rounded-lg overflow-hidden border border-neutral-900 p-4'
id={item.id.toString()}
url={jellyseerrApi?.imageProxy(
item.image,
COMPANY_LOGO_IMAGE_FILTER,
)}
url={seerrApi?.imageProxy(item.image, COMPANY_LOGO_IMAGE_FILTER)}
/>
</TouchableOpacity>
)}

View File

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

View File

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

View File

@@ -3,23 +3,19 @@ import { uniqBy } from "lodash";
import type React from "react";
import { useMemo } from "react";
import type { ViewProps } from "react-native";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import {
type DiscoverEndpoint,
Endpoints,
useJellyseerr,
} from "@/hooks/useJellyseerr";
import SeerrPoster from "@/components/posters/SeerrPoster";
import Slide, { type SlideProps } from "@/components/seerr/discover/Slide";
import { type DiscoverEndpoint, Endpoints, useSeerr } from "@/hooks/useSeerr";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
slide,
...props
}) => {
const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr();
const { seerrApi, isSeerrMovieOrTvResult } = useSeerr();
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "discover", slide.id],
queryKey: ["seerr", "discover", slide.id],
queryFn: async ({ pageParam }) => {
let endpoint: DiscoverEndpoint | undefined;
let params: any = {
@@ -50,13 +46,13 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
break;
}
return endpoint ? jellyseerrApi?.discover(endpoint, params) : null;
return endpoint ? seerrApi?.discover(endpoint, params) : null;
},
initialPageParam: 1,
getNextPageParam: (lastPage, pages) =>
(lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
1,
enabled: !!jellyseerrApi,
enabled: !!seerrApi,
staleTime: 0,
});
@@ -65,9 +61,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
uniqBy(
data?.pages
?.filter((p) => p?.results.length)
.flatMap((p) =>
p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)),
),
.flatMap((p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r))),
"id",
),
[data],
@@ -84,7 +78,7 @@ const MovieTvSlide: React.FC<SlideProps & ViewProps> = ({
onEndReached={() => {
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 type React from "react";
import type { ViewProps } from "react-native";
import Slide, { type SlideProps } from "@/components/jellyseerr/discover/Slide";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import SeerrPoster from "@/components/posters/SeerrPoster";
import Slide, { type SlideProps } from "@/components/seerr/discover/Slide";
import { useSeerr } from "@/hooks/useSeerr";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { NonFunctionProperties } from "@/utils/jellyseerr/server/interfaces/api/common";
@@ -16,36 +16,36 @@ type ExtendedMediaRequest = NonFunctionProperties<MediaRequest> & {
const RequestCard: React.FC<{ request: ExtendedMediaRequest }> = ({
request,
}) => {
const { jellyseerrApi } = useJellyseerr();
const { seerrApi } = useSeerr();
const { data: details } = useQuery({
queryKey: [
"jellyseerr",
"seerr",
"detail",
request.media.mediaType,
request.media.tmdbId,
],
queryFn: async () => {
return request.media.mediaType === MediaType.MOVIE
? jellyseerrApi?.movieDetails(request.media.tmdbId)
: jellyseerrApi?.tvDetails(request.media.tmdbId);
? seerrApi?.movieDetails(request.media.tmdbId)
: seerrApi?.tvDetails(request.media.tmdbId);
},
enabled: !!jellyseerrApi,
enabled: !!seerrApi,
refetchOnMount: true,
staleTime: 0,
});
const { data: refreshedRequest } = useQuery({
queryKey: ["jellyseerr", "requests", request.media.mediaType, request.id],
queryFn: async () => jellyseerrApi?.getRequest(request.id),
enabled: !!jellyseerrApi,
queryKey: ["seerr", "requests", request.media.mediaType, request.id],
queryFn: async () => seerrApi?.getRequest(request.id),
enabled: !!seerrApi,
refetchOnMount: true,
refetchInterval: 5000,
staleTime: 0,
});
return (
<JellyseerrPoster
<SeerrPoster
horizontal
showDownloadInfo
item={details}
@@ -58,12 +58,12 @@ const RecentRequestsSlide: React.FC<SlideProps & ViewProps> = ({
slide,
...props
}) => {
const { jellyseerrApi } = useJellyseerr();
const { seerrApi } = useSeerr();
const { data: requests } = useQuery({
queryKey: ["jellyseerr", "recent_requests"],
queryFn: async () => jellyseerrApi?.requests(),
enabled: !!jellyseerrApi,
queryKey: ["seerr", "recent_requests"],
queryFn: async () => seerrApi?.requests(),
enabled: !!seerrApi,
refetchOnMount: true,
staleTime: 0,
});

View File

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