diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2111dd2e..1037334b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,7 +3,7 @@ ## Project Overview 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. ## Main Technologies diff --git a/README.md b/README.md index 44a8863c..26f8e2fa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@     - +

diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index a7e059ed..761e868c 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -222,9 +222,9 @@ export default function IndexLayout() { }} /> - + ); diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx similarity index 75% rename from app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/company/[companyId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx index fdcd786c..359ebfd1 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/company/[companyId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/company/[companyId].tsx @@ -3,9 +3,9 @@ import { Image } from "expo-image"; import { useLocalSearchParams } from "expo-router"; import { uniqBy } from "lodash"; import { useMemo } from "react"; -import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; -import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; +import SeerrPoster from "@/components/posters/SeerrPoster"; +import ParallaxSlideShow from "@/components/seerr/ParallaxSlideShow"; +import { Endpoints, useSeerr } from "@/hooks/useSeerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; import { type MovieResult, @@ -13,9 +13,9 @@ import { } from "@/utils/jellyseerr/server/models/Search"; import { COMPANY_LOGO_IMAGE_FILTER } from "@/utils/jellyseerr/src/components/Discover/NetworkSlider"; -export default function page() { +export default function CompanyPage() { const local = useLocalSearchParams(); - const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); + const { seerrApi, isSeerrMovieOrTvResult } = useSeerr(); const { companyId, image, type } = local as unknown as { companyId: string; @@ -25,12 +25,12 @@ export default function page() { }; const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery({ - queryKey: ["jellyseerr", "company", type, companyId], + queryKey: ["seerr", "company", type, companyId], queryFn: async ({ pageParam }) => { const params: any = { page: Number(pageParam), }; - return jellyseerrApi?.discover( + return seerrApi?.discover( `${ Number(type) === DiscoverSliderType.NETWORKS ? Endpoints.DISCOVER_TV_NETWORK @@ -39,7 +39,7 @@ export default function page() { params, ); }, - enabled: !!jellyseerrApi && !!companyId, + enabled: !!seerrApi && !!companyId, initialPageParam: 1, getNextPageParam: (lastPage, pages) => (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) + @@ -53,8 +53,7 @@ export default function page() { data?.pages ?.filter((p) => p?.results.length) .flatMap( - (p) => - p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [], + (p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r)) ?? [], ), "id", ) ?? [], @@ -63,15 +62,15 @@ export default function page() { const backdrops = useMemo( () => - jellyseerrApi + seerrApi ? flatData.map((r) => - jellyseerrApi.imageProxy( + seerrApi.imageProxy( (r as TvResult | MovieResult).backdropPath, "w1920_and_h800_multi_faces", ), ) : [], - [jellyseerrApi, flatData], + [seerrApi, flatData], ); return ( @@ -92,7 +91,7 @@ export default function page() { key={companyId} className='bottom-1 w-1/2' source={{ - uri: jellyseerrApi?.imageProxy(image, COMPANY_LOGO_IMAGE_FILTER), + uri: seerrApi?.imageProxy(image, COMPANY_LOGO_IMAGE_FILTER), }} cachePolicy={"memory-disk"} contentFit='contain' @@ -101,7 +100,7 @@ export default function page() { }} /> } - renderItem={(item, _index) => } + renderItem={(item, _index) => } /> ); } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx similarity index 67% rename from app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/genre/[genreId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx index 7ea00808..a61cff40 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/genre/[genreId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/genre/[genreId].tsx @@ -3,15 +3,15 @@ import { useLocalSearchParams } from "expo-router"; import { uniqBy } from "lodash"; import { useMemo } from "react"; import { Text } from "@/components/common/Text"; -import { textShadowStyle } from "@/components/jellyseerr/discover/GenericSlideCard"; -import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; -import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import { Endpoints, useJellyseerr } from "@/hooks/useJellyseerr"; +import SeerrPoster from "@/components/posters/SeerrPoster"; +import { textShadowStyle } from "@/components/seerr/discover/GenericSlideCard"; +import ParallaxSlideShow from "@/components/seerr/ParallaxSlideShow"; +import { Endpoints, useSeerr } from "@/hooks/useSeerr"; import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; -export default function page() { +export default function GenrePage() { const local = useLocalSearchParams(); - const { jellyseerrApi, isJellyseerrMovieOrTvResult } = useJellyseerr(); + const { seerrApi, isSeerrMovieOrTvResult } = useSeerr(); const { genreId, name, type } = local as unknown as { genreId: string; @@ -20,21 +20,21 @@ export default function page() { }; const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ - queryKey: ["jellyseerr", "company", type, genreId], + queryKey: ["seerr", "company", type, genreId], queryFn: async ({ pageParam }) => { const params: any = { page: Number(pageParam), genre: genreId, }; - return jellyseerrApi?.discover( + return seerrApi?.discover( type === DiscoverSliderType.MOVIE_GENRES ? Endpoints.DISCOVER_MOVIES : Endpoints.DISCOVER_TV, params, ); }, - enabled: !!jellyseerrApi && !!genreId, + enabled: !!seerrApi && !!genreId, initialPageParam: 1, getNextPageParam: (lastPage, pages) => (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) + @@ -48,8 +48,7 @@ export default function page() { data?.pages ?.filter((p) => p?.results.length) .flatMap( - (p) => - p?.results.filter((r) => isJellyseerrMovieOrTvResult(r)) ?? [], + (p) => p?.results.filter((r) => isSeerrMovieOrTvResult(r)) ?? [], ), "id", ) ?? [], @@ -58,15 +57,12 @@ export default function page() { const backdrops = useMemo( () => - jellyseerrApi + seerrApi ? flatData.map((r) => - jellyseerrApi.imageProxy( - r.backdropPath, - "w1920_and_h800_multi_faces", - ), + seerrApi.imageProxy(r.backdropPath, "w1920_and_h800_multi_faces"), ) : [], - [jellyseerrApi, flatData], + [seerrApi, flatData], ); return ( @@ -91,7 +87,7 @@ export default function page() { {name} } - renderItem={(item, _index) => } + renderItem={(item, _index) => } /> ); } diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/page.tsx similarity index 87% rename from app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/page.tsx index 2f0af594..901f5032 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/page.tsx @@ -18,18 +18,18 @@ import { toast } from "sonner-native"; import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; 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 { ParallaxScrollView } from "@/components/ParallaxPage"; import { PlatformDropdown } from "@/components/PlatformDropdown"; -import { JellyserrRatings } from "@/components/Ratings"; -import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; +import { SeerrRatings } from "@/components/Ratings"; +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 useRouter from "@/hooks/useAppRouter"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; -import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; +import { useSeerr } from "@/hooks/useSeerr"; +import { useSeerrCanRequest } from "@/utils/_seerr/useSeerrCanRequest"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { type IssueType, @@ -68,7 +68,7 @@ const Page: React.FC = () => { } & Partial; const navigation = useNavigation(); - const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr(); + const { seerrApi, seerrUser, requestMedia } = useSeerr(); const [issueType, setIssueType] = useState(); const [issueMessage, setIssueMessage] = useState(); @@ -83,8 +83,8 @@ const Page: React.FC = () => { isLoading, refetch, } = useQuery({ - enabled: !!jellyseerrApi && !!result && !!result.id, - queryKey: ["jellyseerr", "detail", mediaType, result.id], + enabled: !!seerrApi && !!result && !!result.id, + queryKey: ["seerr", "detail", mediaType, result.id], staleTime: 0, refetchOnMount: true, refetchOnReconnect: true, @@ -93,21 +93,18 @@ const Page: React.FC = () => { refetchInterval: 0, queryFn: async () => { return mediaType === MediaType.MOVIE - ? jellyseerrApi?.movieDetails(result.id!) - : jellyseerrApi?.tvDetails(result.id!); + ? seerrApi?.movieDetails(result.id!) + : seerrApi?.tvDetails(result.id!); }, }); const [canRequest, hasAdvancedRequestPermission] = - useJellyseerrCanRequest(details); + useSeerrCanRequest(details); const canManageRequests = useMemo(() => { - if (!jellyseerrUser) return false; - return hasPermission( - Permission.MANAGE_REQUESTS, - jellyseerrUser.permissions, - ); - }, [jellyseerrUser]); + if (!seerrUser) return false; + return hasPermission(Permission.MANAGE_REQUESTS, seerrUser.permissions); + }, [seerrUser]); const pendingRequest = useMemo(() => { return details?.mediaInfo?.requests?.find( @@ -119,27 +116,27 @@ const Page: React.FC = () => { if (!pendingRequest?.id) return; try { - await jellyseerrApi?.approveRequest(pendingRequest.id); - toast.success(t("jellyseerr.toasts.request_approved")); + await seerrApi?.approveRequest(pendingRequest.id); + toast.success(t("seerr.toasts.request_approved")); refetch(); } 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); } - }, [jellyseerrApi, pendingRequest, refetch, t]); + }, [seerrApi, pendingRequest, refetch, t]); const handleDeclineRequest = useCallback(async () => { if (!pendingRequest?.id) return; try { - await jellyseerrApi?.declineRequest(pendingRequest.id); - toast.success(t("jellyseerr.toasts.request_declined")); + await seerrApi?.declineRequest(pendingRequest.id); + toast.success(t("seerr.toasts.request_declined")); refetch(); } 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); } - }, [jellyseerrApi, pendingRequest, refetch, t]); + }, [seerrApi, pendingRequest, refetch, t]); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( @@ -154,7 +151,7 @@ const Page: React.FC = () => { const submitIssue = useCallback(() => { if (result.id && issueType && issueMessage && details) { - jellyseerrApi + seerrApi ?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage) .then(() => { setIssueType(undefined); @@ -162,7 +159,7 @@ const Page: React.FC = () => { bottomSheetModalRef?.current?.close(); }); } - }, [jellyseerrApi, details, result, issueType, issueMessage]); + }, [seerrApi, details, result, issueType, issueMessage]); const handleIssueModalDismiss = useCallback(() => { setIssueTypeDropdownOpen(false); @@ -214,7 +211,7 @@ const Page: React.FC = () => { const issueTypeOptionGroups = useMemo( () => [ { - title: t("jellyseerr.types"), + title: t("seerr.types"), options: Object.entries(IssueTypeName) .reverse() .map(([key, value]) => ({ @@ -265,7 +262,7 @@ const Page: React.FC = () => { height: "100%", }} source={{ - uri: jellyseerrApi?.imageProxy( + uri: seerrApi?.imageProxy( result.backdropPath, "w1920_and_h800_multi_faces", ), @@ -295,7 +292,7 @@ const Page: React.FC = () => { - { /> ) : canRequest ? ( ) : ( details?.mediaInfo?.jellyfinMediaId && ( @@ -353,7 +350,7 @@ const Page: React.FC = () => { }} > - {t("jellyseerr.report_issue_button")} + {t("seerr.report_issue_button")} )} @@ -389,12 +386,12 @@ const Page: React.FC = () => { - {t("jellyseerr.requested_by", { + {t("seerr.requested_by", { user: pendingRequest.requestedBy?.displayName || pendingRequest.requestedBy?.username || pendingRequest.requestedBy?.jellyfinUsername || - t("jellyseerr.unknown_user"), + t("seerr.unknown_user"), })} @@ -415,7 +412,7 @@ const Page: React.FC = () => { borderStyle: "solid", }} > - {t("jellyseerr.approve")} + {t("seerr.approve")} @@ -442,7 +439,7 @@ const Page: React.FC = () => { {mediaType === MediaType.TV && ( - { - {t("jellyseerr.whats_wrong")} + {t("seerr.whats_wrong")} - {t("jellyseerr.issue_type")} + {t("seerr.issue_type")} { {issueType ? IssueTypeName[issueType] - : t("jellyseerr.select_an_issue")} + : t("seerr.select_an_issue")} } - title={t("jellyseerr.types")} + title={t("seerr.types")} open={issueTypeDropdownOpen} onOpenChange={setIssueTypeDropdownOpen} /> @@ -522,7 +519,7 @@ const Page: React.FC = () => { maxLength={254} style={{ color: "white" }} clearButtonMode='always' - placeholder={t("jellyseerr.describe_the_issue")} + placeholder={t("seerr.describe_the_issue")} placeholderTextColor='#9CA3AF' // Issue with multiline + Textinput inside a portal // https://github.com/callstack/react-native-paper/issues/1668 @@ -532,7 +529,7 @@ const Page: React.FC = () => { diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/person/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/person/[personId].tsx similarity index 70% rename from app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/person/[personId].tsx rename to app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/person/[personId].tsx index a29e1280..41b5ef8f 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/jellyseerr/person/[personId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites,watchlists)/seerr/person/[personId].tsx @@ -5,31 +5,27 @@ import { orderBy, uniqBy } from "lodash"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Text } from "@/components/common/Text"; -import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import { OverviewText } from "@/components/OverviewText"; -import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; +import SeerrPoster from "@/components/posters/SeerrPoster"; +import ParallaxSlideShow from "@/components/seerr/ParallaxSlideShow"; +import { useSeerr } from "@/hooks/useSeerr"; import type { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person"; -export default function page() { +export default function PersonPage() { const local = useLocalSearchParams(); const { t } = useTranslation(); - const { - jellyseerrApi, - jellyseerrRegion: region, - jellyseerrLocale: locale, - } = useJellyseerr(); + const { seerrApi, seerrRegion: region, seerrLocale: locale } = useSeerr(); const { personId } = local as { personId: string }; const { data } = useQuery({ - queryKey: ["jellyseerr", "person", personId], + queryKey: ["seerr", "person", personId], queryFn: async () => ({ - details: await jellyseerrApi?.personDetails(personId), - combinedCredits: await jellyseerrApi?.personCombinedCredits(personId), + details: await seerrApi?.personDetails(personId), + combinedCredits: await seerrApi?.personCombinedCredits(personId), }), - enabled: !!jellyseerrApi && !!personId, + enabled: !!seerrApi && !!personId, }); const castedRoles: PersonCreditCast[] = useMemo( @@ -46,22 +42,19 @@ export default function page() { ); const backdrops = useMemo( () => - jellyseerrApi + seerrApi ? castedRoles.map((c) => - jellyseerrApi.imageProxy( - c.backdropPath, - "w1920_and_h800_multi_faces", - ), + seerrApi.imageProxy(c.backdropPath, "w1920_and_h800_multi_faces"), ) : [], - [jellyseerrApi, data?.combinedCredits], + [seerrApi, data?.combinedCredits], ); return ( item.id.toString()} logo={ {data?.details?.name} - {t("jellyseerr.born")}{" "} + {t("seerr.born")}{" "} {data?.details?.birthday && new Date(data.details.birthday).toLocaleDateString( `${locale}-${region}`, @@ -103,7 +96,7 @@ export default function page() { MainContent={() => ( )} - renderItem={(item, _index) => } + renderItem={(item, _index) => } /> ); } diff --git a/app/(auth)/(tabs)/(search)/_layout.tsx b/app/(auth)/(tabs)/(search)/_layout.tsx index f4d34978..b008c04d 100644 --- a/app/(auth)/(tabs)/(search)/_layout.tsx +++ b/app/(auth)/(tabs)/(search)/_layout.tsx @@ -33,17 +33,17 @@ export default function SearchLayout() { headerShadowVisible: false, }} /> - + diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 751b1df1..cff07dd4 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -26,18 +26,18 @@ import { Input } from "@/components/common/Input"; import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { ItemCardText } from "@/components/ItemCardText"; -import { - JellyseerrSearchSort, - JellyserrIndexPage, -} from "@/components/jellyseerr/JellyseerrIndexPage"; import MoviePoster from "@/components/posters/MoviePoster"; import SeriesPoster from "@/components/posters/SeriesPoster"; import { DiscoverFilters } from "@/components/search/DiscoverFilters"; import { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; import { SearchTabButtons } from "@/components/search/SearchTabButtons"; +import { + SeerrIndexPage, + SeerrSearchSort, +} from "@/components/seerr/SeerrIndexPage"; import useRouter from "@/hooks/useAppRouter"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useSeerr } from "@/hooks/useSeerr"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { eventBus } from "@/utils/eventBus"; @@ -55,7 +55,7 @@ const exampleSearches = [ "The Mandalorian", ]; -export default function search() { +export default function SearchPage() { const params = useLocalSearchParams(); const insets = useSafeAreaInsets(); const router = useRouter(); @@ -93,16 +93,11 @@ export default function search() { const [api] = useAtom(apiAtom); const { settings } = useSettings(); - const { jellyseerrApi } = useJellyseerr(); - const [jellyseerrOrderBy, setJellyseerrOrderBy] = - useState( - JellyseerrSearchSort[ - JellyseerrSearchSort.DEFAULT - ] as unknown as JellyseerrSearchSort, - ); - const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState< - "asc" | "desc" - >("desc"); + const { seerrApi } = useSeerr(); + const [seerrOrderBy, setSeerrOrderBy] = useState( + SeerrSearchSort[SeerrSearchSort.DEFAULT] as unknown as SeerrSearchSort, + ); + const [seerrSortOrder, setSeerrSortOrder] = useState<"asc" | "desc">("desc"); const searchEngine = useMemo(() => { return settings?.searchEngine || "Jellyfin"; @@ -474,7 +469,7 @@ export default function search() { className='flex flex-col' style={{ paddingTop: Platform.OS === "android" ? 10 : 0 }} > - {jellyseerrApi && ( + {seerrApi && ( )} @@ -754,10 +749,10 @@ export default function search() { /> ) : ( - )} diff --git a/assets/icons/jellyseerr-logo.svg b/assets/icons/seerr-logo.svg similarity index 100% rename from assets/icons/jellyseerr-logo.svg rename to assets/icons/seerr-logo.svg diff --git a/assets/images/jellyseerr.PNG b/assets/images/seerr.PNG similarity index 100% rename from assets/images/jellyseerr.PNG rename to assets/images/seerr.PNG diff --git a/components/IntroSheet.tsx b/components/IntroSheet.tsx index ea1a23a2..7317b7e8 100644 --- a/components/IntroSheet.tsx +++ b/components/IntroSheet.tsx @@ -89,16 +89,16 @@ export const IntroSheet = forwardRef((_, ref) => { - Jellyseerr + Seerr - {t("home.intro.jellyseerr_feature_description")} + {t("home.intro.seerr_feature_description")} diff --git a/components/Ratings.tsx b/components/Ratings.tsx index 5741233f..e091c64c 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -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 = ({ 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 ( diff --git a/components/common/JellyseerrItemRouter.tsx b/components/common/SeerrItemRouter.tsx similarity index 92% rename from components/common/JellyseerrItemRouter.tsx rename to components/common/SeerrItemRouter.tsx index f6878bc5..184ef490 100644 --- a/components/common/JellyseerrItemRouter.tsx +++ b/components/common/SeerrItemRouter.tsx @@ -21,7 +21,7 @@ interface Props extends TouchableOpacityProps { mediaType: MediaType; } -export const TouchableJellyseerrRouter: React.FC> = ({ +export const TouchableSeerrRouter: React.FC> = ({ result, mediaTitle, releaseYear, @@ -43,7 +43,7 @@ export const TouchableJellyseerrRouter: React.FC> = ({ if (!result) return; router.push({ - pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, + pathname: `/(auth)/(tabs)/${from}/seerr/page`, // @ts-expect-error params: { ...result, diff --git a/components/music/MusicAlbumCard.tsx b/components/music/MusicAlbumCard.tsx index 1a747c8e..11e78610 100644 --- a/components/music/MusicAlbumCard.tsx +++ b/components/music/MusicAlbumCard.tsx @@ -23,10 +23,7 @@ export const MusicAlbumCard: React.FC = ({ 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 ( diff --git a/components/music/MusicAlbumRowCard.tsx b/components/music/MusicAlbumRowCard.tsx index 6612a392..e794a793 100644 --- a/components/music/MusicAlbumRowCard.tsx +++ b/components/music/MusicAlbumRowCard.tsx @@ -24,10 +24,7 @@ export const MusicAlbumRowCard: React.FC = ({ album }) => { ); const handlePress = useCallback(() => { - router.push({ - pathname: "/music/album/[albumId]", - params: { albumId: album.Id! }, - }); + router.push(`/music/album/${album.Id}`); }, [router, album.Id]); return ( diff --git a/components/music/MusicArtistCard.tsx b/components/music/MusicArtistCard.tsx index c98e3fce..a9bfc61b 100644 --- a/components/music/MusicArtistCard.tsx +++ b/components/music/MusicArtistCard.tsx @@ -25,10 +25,7 @@ export const MusicArtistCard: React.FC = ({ artist }) => { ); const handlePress = useCallback(() => { - router.push({ - pathname: "/music/artist/[artistId]", - params: { artistId: artist.Id! }, - }); + router.push(`/music/artist/${artist.Id}`); }, [router, artist.Id]); return ( diff --git a/components/music/MusicPlaylistCard.tsx b/components/music/MusicPlaylistCard.tsx index 93caf7a4..50671715 100644 --- a/components/music/MusicPlaylistCard.tsx +++ b/components/music/MusicPlaylistCard.tsx @@ -61,10 +61,7 @@ export const MusicPlaylistCard: React.FC = ({ 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 ( diff --git a/components/music/TrackOptionsSheet.tsx b/components/music/TrackOptionsSheet.tsx index a0b78472..b5de1379 100644 --- a/components/music/TrackOptionsSheet.tsx +++ b/components/music/TrackOptionsSheet.tsx @@ -197,10 +197,7 @@ export const TrackOptionsSheet: React.FC = ({ 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 = ({ 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]); diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/SeerrPoster.tsx similarity index 84% rename from components/posters/JellyseerrPoster.tsx rename to components/posters/SeerrPoster.tsx index d7b1fcdb..399ac386 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/SeerrPoster.tsx @@ -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 = ({ +const SeerrPoster: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ }, [mediaRequest, is4k]); return ( - = ({ )} )} - - @@ -201,8 +198,8 @@ const JellyseerrPoster: React.FC = ({ {releaseYear || ""} - + ); }; -export default JellyseerrPoster; +export default SeerrPoster; diff --git a/components/search/DiscoverFilters.tsx b/components/search/DiscoverFilters.tsx index 2e844c88..7a3a2a60 100644 --- a/components/search/DiscoverFilters.tsx +++ b/components/search/DiscoverFilters.tsx @@ -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 = ({ searchFilterId, orderFilterId, - jellyseerrOrderBy, - setJellyseerrOrderBy, - jellyseerrSortOrder, - setJellyseerrSortOrder, + seerrOrderBy, + setSeerrOrderBy, + seerrSortOrder, + setSeerrSortOrder, t, }) => { if (Platform.OS === "ios") { @@ -52,16 +52,16 @@ export const DiscoverFilters: React.FC = ({ - 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 = ({ 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]); }} /> @@ -86,17 +86,15 @@ export const DiscoverFilters: React.FC = ({ - 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 = ({ 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} diff --git a/components/jellyseerr/Cast.tsx b/components/seerr/Cast.tsx similarity index 88% rename from components/jellyseerr/Cast.tsx rename to components/seerr/Cast.tsx index 99440bd8..9063ac65 100644 --- a/components/jellyseerr/Cast.tsx +++ b/components/seerr/Cast.tsx @@ -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 && ( - - {t("jellyseerr.cast")} - + {t("seerr.cast")} = ({ 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 && ( - {t("jellyseerr.details")} + {t("seerr.details")} - + {details.keywords.some( (keyword) => keyword.id === ANIME_KEYWORD_ID, - ) && } + ) && } ( {r.type === 3 ? ( @@ -178,16 +177,13 @@ const DetailFacts: React.FC< ))} /> - - - - - + + + + + ( @@ -196,17 +192,17 @@ const DetailFacts: React.FC< ))} /> n.name, )} /> n.name)} /> s.name)} /> diff --git a/components/jellyseerr/GridSkeleton.tsx b/components/seerr/GridSkeleton.tsx similarity index 100% rename from components/jellyseerr/GridSkeleton.tsx rename to components/seerr/GridSkeleton.tsx diff --git a/components/jellyseerr/ParallaxSlideShow.tsx b/components/seerr/ParallaxSlideShow.tsx similarity index 100% rename from components/jellyseerr/ParallaxSlideShow.tsx rename to components/seerr/ParallaxSlideShow.tsx diff --git a/components/jellyseerr/PersonPoster.tsx b/components/seerr/PersonPoster.tsx similarity index 78% rename from components/jellyseerr/PersonPoster.tsx rename to components/seerr/PersonPoster.tsx index e926b093..821cf52c 100644 --- a/components/jellyseerr/PersonPoster.tsx +++ b/components/seerr/PersonPoster.tsx @@ -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 = ({ 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 = ({ if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( - router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`) - } + onPress={() => router.push(`/(auth)/(tabs)/${from}/seerr/person/${id}`)} > {name} {subName && {subName}} diff --git a/components/jellyseerr/RequestModal.tsx b/components/seerr/RequestModal.tsx similarity index 89% rename from components/jellyseerr/RequestModal.tsx rename to components/seerr/RequestModal.tsx index 5bc44177..97e04619 100644 --- a/components/jellyseerr/RequestModal.tsx +++ b/components/seerr/RequestModal.tsx @@ -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({ 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< - {t("jellyseerr.advanced")} + {t("seerr.advanced")} {seasonTitle && ( {seasonTitle} @@ -319,7 +317,7 @@ const RequestModal = forwardRef< <> - {t("jellyseerr.quality_profile")} + {t("seerr.quality_profile")} } - title={t("jellyseerr.quality_profile")} + title={t("seerr.quality_profile")} open={qualityProfileOpen} onOpenChange={setQualityProfileOpen} /> @@ -343,7 +341,7 @@ const RequestModal = forwardRef< - {t("jellyseerr.root_folder")} + {t("seerr.root_folder")} } - title={t("jellyseerr.root_folder")} + title={t("seerr.root_folder")} open={rootFolderOpen} onOpenChange={setRootFolderOpen} /> @@ -376,7 +374,7 @@ const RequestModal = forwardRef< - {t("jellyseerr.tags")} + {t("seerr.tags")} } - title={t("jellyseerr.tags")} + title={t("seerr.tags")} open={tagsOpen} onOpenChange={setTagsOpen} /> @@ -403,7 +401,7 @@ const RequestModal = forwardRef< - {t("jellyseerr.request_as")} + {t("seerr.request_as")} u.id === - (requestOverrides.userId || jellyseerrUser?.id), - )?.displayName || jellyseerrUser!.displayName} + (requestOverrides.userId || seerrUser?.id), + )?.displayName || seerrUser!.displayName} } - title={t("jellyseerr.request_as")} + title={t("seerr.request_as")} open={usersOpen} onOpenChange={setUsersOpen} /> @@ -427,7 +425,7 @@ const RequestModal = forwardRef< )} diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/seerr/SeerrIndexPage.tsx similarity index 65% rename from components/jellyseerr/JellyseerrIndexPage.tsx rename to components/seerr/SeerrIndexPage.tsx index 42fd223b..6ef54fa6 100644 --- a/components/jellyseerr/JellyseerrIndexPage.tsx +++ b/components/seerr/SeerrIndexPage.tsx @@ -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 = ({ +export const SeerrIndexPage: React.FC = ({ 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 = ({ ), ); }, - enabled: !!jellyseerrApi && searchQuery.length > 0, + enabled: !!seerrApi && searchQuery.length > 0, }); useAnimatedReaction( @@ -92,20 +92,20 @@ export const JellyserrIndexPage: React.FC = ({ 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 = ({ ], 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 ( - + ); @@ -155,9 +151,9 @@ export const JellyserrIndexPage: React.FC = ({ - {!jellyseerrMovieResults?.length && - !jellyseerrTvResults?.length && - !jellyseerrPersonResults?.length && + {!seerrMovieResults?.length && + !seerrTvResults?.length && + !seerrPersonResults?.length && !f1 && !f2 && !l1 && @@ -175,21 +171,21 @@ export const JellyserrIndexPage: React.FC = ({ ( - + )} /> ( - + )} /> ( = ({ 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; diff --git a/components/jellyseerr/JellyseerrStatusIcon.tsx b/components/seerr/SeerrStatusIcon.tsx similarity index 96% rename from components/jellyseerr/JellyseerrStatusIcon.tsx rename to components/seerr/SeerrStatusIcon.tsx index e9b37af5..414fea62 100644 --- a/components/jellyseerr/JellyseerrStatusIcon.tsx +++ b/components/seerr/SeerrStatusIcon.tsx @@ -9,7 +9,7 @@ interface Props { onPress?: () => void; } -const JellyseerrStatusIcon: React.FC = ({ +const SeerrStatusIcon: React.FC = ({ mediaStatus, showRequestIcon, onPress, @@ -74,4 +74,4 @@ const JellyseerrStatusIcon: React.FC = ({ ); }; -export default JellyseerrStatusIcon; +export default SeerrStatusIcon; diff --git a/components/jellyseerr/discover/CompanySlide.tsx b/components/seerr/discover/CompanySlide.tsx similarity index 74% rename from components/jellyseerr/discover/CompanySlide.tsx rename to components/seerr/discover/CompanySlide.tsx index abba1d31..58e614fa 100644 --- a/components/jellyseerr/discover/CompanySlide.tsx +++ b/components/seerr/discover/CompanySlide.tsx @@ -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< )} diff --git a/components/jellyseerr/discover/Discover.tsx b/components/seerr/discover/Discover.tsx similarity index 87% rename from components/jellyseerr/discover/Discover.tsx rename to components/seerr/discover/Discover.tsx index 67e5bf3c..7a926eec 100644 --- a/components/jellyseerr/discover/Discover.tsx +++ b/components/seerr/discover/Discover.tsx @@ -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"; diff --git a/components/jellyseerr/discover/GenericSlideCard.tsx b/components/seerr/discover/GenericSlideCard.tsx similarity index 100% rename from components/jellyseerr/discover/GenericSlideCard.tsx rename to components/seerr/discover/GenericSlideCard.tsx diff --git a/components/jellyseerr/discover/GenreSlide.tsx b/components/seerr/discover/GenreSlide.tsx similarity index 78% rename from components/jellyseerr/discover/GenreSlide.tsx rename to components/seerr/discover/GenreSlide.tsx index 31248540..e65534d0 100644 --- a/components/jellyseerr/discover/GenreSlide.tsx +++ b/components/seerr/discover/GenreSlide.tsx @@ -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 = ({ 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 = ({ 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] diff --git a/components/jellyseerr/discover/MovieTvSlide.tsx b/components/seerr/discover/MovieTvSlide.tsx similarity index 76% rename from components/jellyseerr/discover/MovieTvSlide.tsx rename to components/seerr/discover/MovieTvSlide.tsx index a82f48a3..6291bab1 100644 --- a/components/jellyseerr/discover/MovieTvSlide.tsx +++ b/components/seerr/discover/MovieTvSlide.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ onEndReached={() => { if (hasNextPage) fetchNextPage(); }} - renderItem={(item) => } + renderItem={(item) => } /> ) ); diff --git a/components/jellyseerr/discover/RecentRequestsSlide.tsx b/components/seerr/discover/RecentRequestsSlide.tsx similarity index 70% rename from components/jellyseerr/discover/RecentRequestsSlide.tsx rename to components/seerr/discover/RecentRequestsSlide.tsx index 71b5fc21..12cdef40 100644 --- a/components/jellyseerr/discover/RecentRequestsSlide.tsx +++ b/components/seerr/discover/RecentRequestsSlide.tsx @@ -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 & { 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 ( - = ({ 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, }); diff --git a/components/jellyseerr/discover/Slide.tsx b/components/seerr/discover/Slide.tsx similarity index 100% rename from components/jellyseerr/discover/Slide.tsx rename to components/seerr/discover/Slide.tsx diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/SeerrSeasons.tsx similarity index 88% rename from components/series/JellyseerrSeasons.tsx rename to components/series/SeerrSeasons.tsx index 3b130683..ff1f0b6d 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/SeerrSeasons.tsx @@ -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<{ { const canRequest = season.status === MediaStatus.UNKNOWN; return ( - requestSeason(canRequest, season.seasonNumber) @@ -326,7 +322,7 @@ const JellyseerrSeasons: React.FC<{ {seasonStates?.[season.seasonNumber] && ( - { - 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 ( - - - {jellyseerrUser ? ( - <> - - - - - - - - - - - - - ) : ( - - - {t("home.settings.plugins.jellyseerr.jellyseerr_warning")} - - - {t("home.settings.plugins.jellyseerr.server_url")} - - - - {t("home.settings.plugins.jellyseerr.server_url_hint")} - - - - - - {t("home.settings.plugins.jellyseerr.password")} - - - - - - )} - - - ); -}; diff --git a/components/settings/PluginSettings.tsx b/components/settings/PluginSettings.tsx index 1b21cf98..e6a6d310 100644 --- a/components/settings/PluginSettings.tsx +++ b/components/settings/PluginSettings.tsx @@ -19,7 +19,7 @@ export const PluginSettings = () => { className='mb-4' > router.push("/settings/plugins/jellyseerr/page")} + onPress={() => router.push("/settings/plugins/seerr/page")} title={"Seerr"} showArrow /> diff --git a/components/settings/Seerr.tsx b/components/settings/Seerr.tsx new file mode 100644 index 00000000..1ad87e7f --- /dev/null +++ b/components/settings/Seerr.tsx @@ -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( + undefined, + ); + + const [seerrServerUrl, setSeerrServerUrl] = useState( + 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 ( + + + {seerrUser ? ( + <> + + + + + + + + + + + + + ) : ( + + + {t("home.settings.plugins.seerr.seerr_warning")} + + + {t("home.settings.plugins.seerr.server_url")} + + + + {t("home.settings.plugins.seerr.server_url_hint")} + + + + + + {t("home.settings.plugins.seerr.password")} + + + + + + )} + + + ); +}; diff --git a/hooks/useJellyseerr.ts b/hooks/useSeerr.ts similarity index 82% rename from hooks/useJellyseerr.ts rename to hooks/useSeerr.ts index d1d511fb..55a242a9 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useSeerr.ts @@ -2,7 +2,7 @@ import axios, { type AxiosError, type AxiosInstance } from "axios"; import { atom } from "jotai"; import { useAtom } from "jotai/index"; 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 { MovieResult, Results, @@ -62,12 +62,12 @@ interface SearchResults { results: Results[]; } -const JELLYSEERR_USER = "JELLYSEERR_USER"; -const JELLYSEERR_COOKIES = "JELLYSEERR_COOKIES"; +const SEERR_USER = "SEERR_USER"; +const SEERR_COOKIES = "SEERR_COOKIES"; -export const clearJellyseerrStorageData = () => { - storage.remove(JELLYSEERR_USER); - storage.remove(JELLYSEERR_COOKIES); +export const clearSeerrStorageData = () => { + storage.remove(SEERR_USER); + storage.remove(SEERR_COOKIES); }; export enum Endpoints { @@ -111,7 +111,7 @@ export type TestResult = isValid: false; }; -export class JellyseerrApi { +export class SeerrApi { axios: AxiosInstance; constructor(baseUrl: string) { @@ -126,8 +126,8 @@ export class JellyseerrApi { } async test(): Promise { - const user = storage.get(JELLYSEERR_USER); - const cookies = storage.get(JELLYSEERR_COOKIES); + const user = storage.get(SEERR_USER); + const cookies = storage.get(SEERR_COOKIES); if (user && cookies) { return Promise.resolve({ @@ -142,15 +142,13 @@ export class JellyseerrApi { const { status, headers, data } = response; if (inRange(status, 200, 299)) { if (data.version < "2.0.0") { - const error = t( - "jellyseerr.toasts.jellyseer_does_not_meet_requirements", - ); + const error = t("seerr.toasts.seer_does_not_meet_requirements"); toast.error(error); throw Error(error); } storage.setAny( - JELLYSEERR_COOKIES, + SEERR_COOKIES, headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [], ); return { @@ -158,7 +156,7 @@ export class JellyseerrApi { requiresPass: true, }; } - toast.error(t("jellyseerr.toasts.jellyseerr_test_failed")); + toast.error(t("seerr.toasts.seerr_test_failed")); writeErrorLog( `Seerr returned a ${status} for url:\n${response.config.url}`, response.data, @@ -169,7 +167,7 @@ export class JellyseerrApi { }; }) .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); console.error(msg, e); return { @@ -179,9 +177,9 @@ export class JellyseerrApi { }); } - async login(username: string, password: string): Promise { + async login(username: string, password: string): Promise { return this.axios - ?.post(Endpoints.API_V1 + Endpoints.AUTH_JELLYFIN, { + ?.post(Endpoints.API_V1 + Endpoints.AUTH_JELLYFIN, { username, password, email: username, @@ -189,7 +187,7 @@ export class JellyseerrApi { .then((response) => { const user = response?.data; if (!user) throw Error("Login failed"); - storage.setAny(JELLYSEERR_USER, user); + storage.setAny(SEERR_USER, user); return user; }); } @@ -364,7 +362,7 @@ export class JellyseerrApi { const issue = response.data; if (issue.status === IssueStatus.OPEN) { - toast.success(t("jellyseerr.toasts.issue_submitted")); + toast.success(t("seerr.toasts.issue_submitted")); } return issue; }); @@ -392,7 +390,7 @@ export class JellyseerrApi { const cookies = response.headers["set-cookie"]; if (cookies) { storage.setAny( - JELLYSEERR_COOKIES, + SEERR_COOKIES, response.headers["set-cookie"]?.flatMap((c) => c.split("; ")), ); } @@ -404,7 +402,7 @@ export class JellyseerrApi { error.response?.data, ); if (error.response?.status === 403) { - clearJellyseerrStorageData(); + clearSeerrStorageData(); } return Promise.reject(error); }, @@ -412,7 +410,7 @@ export class JellyseerrApi { this.axios.interceptors.request.use( async (config) => { - const cookies = storage.get(JELLYSEERR_COOKIES); + const cookies = storage.get(SEERR_COOKIES); if (cookies) { const headerName = this.axios.defaults.xsrfHeaderName!; const xsrfToken = cookies @@ -431,59 +429,55 @@ export class JellyseerrApi { } } -const jellyseerrUserAtom = atom(storage.get(JELLYSEERR_USER)); +const seerrUserAtom = atom(storage.get(SEERR_USER)); -export const useJellyseerr = () => { +export const useSeerr = () => { const { settings, updateSettings } = useSettings(); - const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom); + const [seerrUser, setSeerrUser] = useAtom(seerrUserAtom); const queryClient = useNetworkAwareQueryClient(); - const jellyseerrApi = useMemo(() => { - const cookies = storage.get(JELLYSEERR_COOKIES); - if (settings?.jellyseerrServerUrl && cookies && jellyseerrUser) { - return new JellyseerrApi(settings?.jellyseerrServerUrl); + const seerrApi = useMemo(() => { + const cookies = storage.get(SEERR_COOKIES); + if (settings?.seerrServerUrl && cookies && seerrUser) { + return new SeerrApi(settings?.seerrServerUrl); } return undefined; - }, [settings?.jellyseerrServerUrl, jellyseerrUser]); + }, [settings?.seerrServerUrl, seerrUser]); - const clearAllJellyseerData = useCallback(async () => { - clearJellyseerrStorageData(); - setJellyseerrUser(undefined); - updateSettings({ jellyseerrServerUrl: undefined }); + const clearAllSeerrData = useCallback(async () => { + clearSeerrStorageData(); + setSeerrUser(undefined); + updateSettings({ seerrServerUrl: undefined }); }, []); const requestMedia = useCallback( (title: string, request: MediaRequestBody, onSuccess?: () => void) => { - jellyseerrApi?.request?.(request)?.then(async (mediaRequest) => { + seerrApi?.request?.(request)?.then(async (mediaRequest) => { await queryClient.invalidateQueries({ - queryKey: ["search", "jellyseerr"], + queryKey: ["search", "seerr"], }); switch (mediaRequest.status) { case MediaRequestStatus.PENDING: case MediaRequestStatus.APPROVED: - toast.success( - t("jellyseerr.toasts.requested_item", { item: title }), - ); + toast.success(t("seerr.toasts.requested_item", { item: title })); onSuccess?.(); break; case MediaRequestStatus.DECLINED: - toast.error( - t("jellyseerr.toasts.you_dont_have_permission_to_request"), - ); + toast.error(t("seerr.toasts.you_dont_have_permission_to_request")); break; case MediaRequestStatus.FAILED: toast.error( - t("jellyseerr.toasts.something_went_wrong_requesting_media"), + t("seerr.toasts.something_went_wrong_requesting_media"), ); break; } }); }, - [jellyseerrApi], + [seerrApi], ); - const isJellyseerrMovieOrTvResult = ( + const isSeerrMovieOrTvResult = ( items: any | null | undefined, ): items is MovieResult | TvResult => { return ( @@ -496,7 +490,7 @@ export const useJellyseerr = () => { const getTitle = ( item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, ) => { - return isJellyseerrMovieOrTvResult(item) + return isSeerrMovieOrTvResult(item) ? item.mediaType === MediaType.MOVIE ? item?.title : item?.name @@ -509,7 +503,7 @@ export const useJellyseerr = () => { item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, ) => { return new Date( - (isJellyseerrMovieOrTvResult(item) + (isSeerrMovieOrTvResult(item) ? item.mediaType === MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate @@ -522,32 +516,32 @@ export const useJellyseerr = () => { const getMediaType = ( item?: TvResult | TvDetails | MovieResult | MovieDetails | PersonCreditCast, ): MediaType => { - return isJellyseerrMovieOrTvResult(item) + return isSeerrMovieOrTvResult(item) ? (item.mediaType as MediaType) : item?.mediaInfo?.mediaType; }; - const jellyseerrRegion = useMemo( + const seerrRegion = useMemo( // streamingRegion and discoverRegion exists. region doesn't - () => jellyseerrUser?.settings?.discoverRegion || "US", - [jellyseerrUser], + () => seerrUser?.settings?.discoverRegion || "US", + [seerrUser], ); - const jellyseerrLocale = useMemo(() => { - return jellyseerrUser?.settings?.locale || "en"; - }, [jellyseerrUser]); + const seerrLocale = useMemo(() => { + return seerrUser?.settings?.locale || "en"; + }, [seerrUser]); return { - jellyseerrApi, - jellyseerrUser, - setJellyseerrUser, - clearAllJellyseerData, - isJellyseerrMovieOrTvResult, + seerrApi, + seerrUser, + setSeerrUser, + clearAllSeerrData, + isSeerrMovieOrTvResult, getTitle, getYear, getMediaType, - jellyseerrRegion, - jellyseerrLocale, + seerrRegion, + seerrLocale, requestMedia, }; }; diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index da75dcd1..616e5d4e 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -23,7 +23,7 @@ import { getDeviceName } from "react-native-device-info"; import uuid from "react-native-uuid"; import useRouter from "@/hooks/useAppRouter"; import { useInterval } from "@/hooks/useInterval"; -import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; +import { SeerrApi, useSeerr } from "@/hooks/useSeerr"; import { useSettings } from "@/utils/atoms/settings"; import { writeErrorLog, writeInfoLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; @@ -113,7 +113,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const [isPolling, setIsPolling] = useState(false); const [secret, setSecret] = useState(null); const { setPluginSettings, refreshStreamyfinPluginSettings } = useSettings(); - const { clearAllJellyseerData, setJellyseerrUser } = useJellyseerr(); + const { clearAllSeerrData, setSeerrUser } = useSeerr(); const headers = useMemo(() => { if (!deviceId) return {}; @@ -290,13 +290,13 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ } const recentPluginSettings = await refreshStreamyfinPluginSettings(); - if (recentPluginSettings?.jellyseerrServerUrl?.value) { - const jellyseerrApi = new JellyseerrApi( - recentPluginSettings.jellyseerrServerUrl.value, + if (recentPluginSettings?.seerrServerUrl?.value) { + const seerrApi = new SeerrApi( + recentPluginSettings.seerrServerUrl.value, ); - await jellyseerrApi.test().then((result) => { + await seerrApi.test().then((result) => { 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); setApi(null); setPluginSettings(undefined); - await clearAllJellyseerData(); + await clearAllSeerrData(); // Note: We keep saved credentials for quick switching back }, onError: (error) => { diff --git a/translations/en.json b/translations/en.json index 27e38ddb..c6652310 100644 --- a/translations/en.json +++ b/translations/en.json @@ -96,7 +96,7 @@ "a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin", "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:", - "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_description": "Download movies and TV shows to view offline.", "chromecast_feature_description": "Cast movies and TV shows to your Chromecast devices.", @@ -326,8 +326,8 @@ }, "plugins": { "plugins_title": "Plugins", - "jellyseerr": { - "jellyseerr_warning": "This integration is in early development. Features may change.", + "seerr": { + "seerr_warning": "This integration is in early development. Features may change.", "server_url": "Server URL", "server_url_hint": "Example: http(s)://your-host.url\n(add port if required)", "server_url_placeholder": "Seerr URL", @@ -339,7 +339,7 @@ "movie_quota_days": "Movie Quota Days", "tv_quota_limit": "TV Quota Limit", "tv_quota_days": "TV Quota Days", - "reset_jellyseerr_config_button": "Reset Seerr Config", + "reset_seerr_config_button": "Reset Seerr Config", "unlimited": "Unlimited", "plus_n_more": "+{{n}} more", "order_by": { @@ -675,7 +675,7 @@ "for_kids": "For Kids", "news": "News" }, - "jellyseerr": { + "seerr": { "confirm": "Confirm", "cancel": "Cancel", "yes": "Yes", @@ -719,9 +719,9 @@ "requested_by": "Requested by {{user}}", "unknown_user": "Unknown User", "toasts": { - "jellyseer_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.", - "failed_to_test_jellyseerr_server_url": "Failed to test Seerr server URL", + "seer_does_not_meet_requirements": "Seerr server does not meet minimum version requirements! Please update to at least 2.0.0", + "seerr_test_failed": "Seerr test failed. Please try again.", + "failed_to_test_seerr_server_url": "Failed to test Seerr server URL", "issue_submitted": "Issue Submitted!", "requested_item": "Requested {{item}}!", "you_dont_have_permission_to_request": "You don't have permission to request!", diff --git a/utils/_jellyseerr/useJellyseerrCanRequest.ts b/utils/_seerr/useSeerrCanRequest.ts similarity index 83% rename from utils/_jellyseerr/useJellyseerrCanRequest.ts rename to utils/_seerr/useSeerrCanRequest.ts index 2cd67a33..15e395db 100644 --- a/utils/_jellyseerr/useJellyseerrCanRequest.ts +++ b/utils/_seerr/useSeerrCanRequest.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useSeerr } from "@/hooks/useSeerr"; import { MediaRequestStatus, MediaStatus, @@ -18,13 +18,13 @@ import type MediaRequest from "../jellyseerr/server/entity/MediaRequest"; import type { MovieDetails } from "../jellyseerr/server/models/Movie"; import type { TvDetails } from "../jellyseerr/server/models/Tv"; -export const useJellyseerrCanRequest = ( +export const useSeerrCanRequest = ( item?: MovieResult | TvResult | MovieDetails | TvDetails | PersonCreditCast, ) => { - const { jellyseerrUser } = useJellyseerr(); + const { seerrUser } = useSeerr(); const canRequest = useMemo(() => { - if (!jellyseerrUser || !item) return false; + if (!seerrUser || !item) return false; const canNotRequest = item?.mediaInfo?.requests?.some( @@ -46,22 +46,22 @@ export const useJellyseerrCanRequest = ( ? Permission.REQUEST_MOVIE : Permission.REQUEST_TV, ], - jellyseerrUser.permissions, + seerrUser.permissions, { type: "or" }, ); return userHasPermission && !canNotRequest; - }, [item, jellyseerrUser]); + }, [item, seerrUser]); const hasAdvancedRequestPermission = useMemo(() => { - if (!jellyseerrUser) return false; + if (!seerrUser) return false; return hasPermission( [Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS], - jellyseerrUser.permissions, + seerrUser.permissions, { type: "or" }, ); - }, [jellyseerrUser]); + }, [seerrUser]); return [canRequest, hasAdvancedRequestPermission]; }; diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 28f7d1b4..ecfd7a74 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -174,7 +174,7 @@ export type Settings = { disableHapticFeedback: boolean; subtitleSize: number; safeAreaInControlsEnabled: boolean; - jellyseerrServerUrl?: string; + seerrServerUrl?: string; useKefinTweaks: boolean; hiddenLibraries?: string[]; enableH265ForChromecast: boolean; @@ -259,7 +259,7 @@ export const defaultValues: Settings = { disableHapticFeedback: false, subtitleSize: 100, // Scale value * 100, so 100 = 1.0x safeAreaInControlsEnabled: true, - jellyseerrServerUrl: undefined, + seerrServerUrl: undefined, useKefinTweaks: false, hiddenLibraries: [], enableH265ForChromecast: false,