Jellyseerr

This commit is contained in:
Simon Caron
2025-01-13 00:03:41 -05:00
parent 90f20f6e46
commit cd8aba32d8
10 changed files with 106 additions and 35 deletions

View File

@@ -1,9 +1,10 @@
import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { t } from "i18next"; import { useTranslation } from "react-i18next";
export default function SearchLayout() { export default function SearchLayout() {
const { t } = useTranslation();
return ( return (
<Stack> <Stack>
<Stack.Screen <Stack.Screen

View File

@@ -13,9 +13,12 @@ import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow"; import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
import { useTranslation } from "react-i18next";
export default function page() { export default function page() {
const local = useLocalSearchParams(); const local = useLocalSearchParams();
const { t } = useTranslation();
const { jellyseerrApi, jellyseerrUser } = useJellyseerr(); const { jellyseerrApi, jellyseerrUser } = useJellyseerr();
const { personId } = local as { personId: string }; const { personId } = local as { personId: string };
@@ -58,7 +61,7 @@ export default function page() {
<ParallaxSlideShow <ParallaxSlideShow
data={castedRoles} data={castedRoles}
images={backdrops} images={backdrops}
listHeader="Appearances" listHeader={t("jellyseerr.appearances")}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
logo={ logo={
<Image <Image
@@ -85,7 +88,7 @@ export default function page() {
{data?.details?.name} {data?.details?.name}
</Text> </Text>
<Text className="opacity-50"> <Text className="opacity-50">
Born{" "} {t("jellyseerr.born")}{" "}
{new Date(data?.details?.birthday!!).toLocaleDateString( {new Date(data?.details?.birthday!!).toLocaleDateString(
`${locale}-${region}`, `${locale}-${region}`,
{ {

View File

@@ -16,9 +16,11 @@ import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useEffect, useMemo } from "react"; import React, { useEffect, useMemo } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { useTranslation } from "react-i18next";
const page: React.FC = () => { const page: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { t } = useTranslation();
const params = useLocalSearchParams(); const params = useLocalSearchParams();
const { id: seriesId, seasonIndex } = params as { const { id: seriesId, seasonIndex } = params as {
id: string; id: string;
@@ -85,7 +87,7 @@ const page: React.FC = () => {
<AddToFavorites item={item} type="series" /> <AddToFavorites item={item} type="series" />
<DownloadItems <DownloadItems
size="large" size="large"
title="Download Series" title={t("item_card.download.download_series")}
items={allEpisodes || []} items={allEpisodes || []}
MissingDownloadIconComponent={() => ( MissingDownloadIconComponent={() => (
<Ionicons name="download" size={22} color="white" /> <Ionicons name="download" size={22} color="white" />

View File

@@ -32,7 +32,7 @@ import { MediaSourceSelector } from "./MediaSourceSelector";
import ProgressCircle from "./ProgressCircle"; import ProgressCircle from "./ProgressCircle";
import { RoundButton } from "./RoundButton"; import { RoundButton } from "./RoundButton";
import { SubtitleTrackSelector } from "./SubtitleTrackSelector"; import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
import { useTranslation } from "react-i18next"; import { t } from "i18next";
interface DownloadProps extends ViewProps { interface DownloadProps extends ViewProps {
items: BaseItemDto[]; items: BaseItemDto[];
@@ -56,7 +56,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const [queue, setQueue] = useAtom(queueAtom); const [queue, setQueue] = useAtom(queueAtom);
const [settings] = useSettings(); const [settings] = useSettings();
const { t } = useTranslation();
const { processes, startBackgroundDownload, downloadedFiles } = useDownload(); const { processes, startBackgroundDownload, downloadedFiles } = useDownload();
const { startRemuxing } = useRemuxHlsToMp4(); const { startRemuxing } = useRemuxHlsToMp4();
@@ -393,7 +393,9 @@ export const DownloadSingleItem: React.FC<{
return ( return (
<DownloadItems <DownloadItems
size={size} size={size}
title="Download Episode" title={item.Type == "Episode"
? t("item_card.download.download_episode")
: t("item_card.download.download_movie")}
subtitle={item.Name!} subtitle={item.Name!}
items={[item]} items={[item]}
MissingDownloadIconComponent={() => ( MissingDownloadIconComponent={() => (

View File

@@ -5,15 +5,17 @@ import React from "react";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import PersonPoster from "@/components/jellyseerr/PersonPoster"; import PersonPoster from "@/components/jellyseerr/PersonPoster";
import { useTranslation } from "react-i18next";
const CastSlide: React.FC< const CastSlide: React.FC<
{ details?: MovieDetails | TvDetails } & ViewProps { details?: MovieDetails | TvDetails } & ViewProps
> = ({ details, ...props }) => { > = ({ details, ...props }) => {
const { t } = useTranslation();
return ( return (
details?.credits?.cast && details?.credits?.cast &&
details?.credits?.cast?.length > 0 && ( details?.credits?.cast?.length > 0 && (
<View {...props}> <View {...props}>
<Text className="text-lg font-bold mb-2 px-4">Cast</Text> <Text className="text-lg font-bold mb-2 px-4">{t("jellyseerr.cast")}</Text>
<FlashList <FlashList
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}

View File

@@ -9,6 +9,7 @@ import { TmdbRelease } from "@/utils/jellyseerr/server/api/themoviedb/interfaces
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import CountryFlag from "react-native-country-flag"; import CountryFlag from "react-native-country-flag";
import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants"; import { ANIME_KEYWORD_ID } from "@/utils/jellyseerr/server/api/themoviedb/constants";
import { useTranslation } from "react-i18next";
interface Release { interface Release {
certification: string; certification: string;
@@ -50,6 +51,7 @@ const DetailFacts: React.FC<
{ details?: MovieDetails | TvDetails } & ViewProps { details?: MovieDetails | TvDetails } & ViewProps
> = ({ details, className, ...props }) => { > = ({ details, className, ...props }) => {
const { jellyseerrUser } = useJellyseerr(); const { jellyseerrUser } = useJellyseerr();
const { t } = useTranslation();
const locale = useMemo(() => { const locale = useMemo(() => {
return jellyseerrUser?.settings?.locale || "en"; return jellyseerrUser?.settings?.locale || "en";
@@ -144,21 +146,21 @@ const DetailFacts: React.FC<
return ( return (
details && ( details && (
<View className="p-4"> <View className="p-4">
<Text className="text-lg font-bold">Details</Text> <Text className="text-lg font-bold">{t("jellyseerr.details")}</Text>
<View <View
className={`${className} flex flex-col justify-center divide-y-2 divide-neutral-800`} className={`${className} flex flex-col justify-center divide-y-2 divide-neutral-800`}
{...props} {...props}
> >
<Fact title="Status" fact={details?.status} /> <Fact title={t("jellyseerr.status")} fact={details?.status} />
<Fact <Fact
title="Original Title" title={t("jellyseerr.original_title")}
fact={(details as TvDetails)?.originalName} fact={(details as TvDetails)?.originalName}
/> />
{details.keywords.some( {details.keywords.some(
(keyword) => keyword.id === ANIME_KEYWORD_ID (keyword) => keyword.id === ANIME_KEYWORD_ID
) && <Fact title="Series Type" fact="Anime" />} ) && <Fact title={t("jellyseerr.series_type")} fact="Anime" />}
<Facts <Facts
title="Release Dates" title={t("jellyseerr.release_dates")}
facts={filteredReleases?.map?.((r: Release, idx) => ( facts={filteredReleases?.map?.((r: Release, idx) => (
<View key={idx} className="flex flex-row space-x-2 items-center"> <View key={idx} className="flex flex-row space-x-2 items-center">
{r.type === 3 ? ( {r.type === 3 ? (
@@ -184,13 +186,13 @@ const DetailFacts: React.FC<
</View> </View>
))} ))}
/> />
<Fact title="First Air Date" fact={firstAirDate} /> <Fact title={t("jellyseerr.first_air_date")} fact={firstAirDate} />
<Fact title="Next Air Date" fact={nextAirDate} /> <Fact title={t("jellyseerr.next_air_date")} fact={nextAirDate} />
<Fact title="Revenue" fact={revenue} /> <Fact title={t("jellyseerr.revenue")} fact={revenue} />
<Fact title="Budget" fact={budget} /> <Fact title={t("jellyseerr.budget")} fact={budget} />
<Fact title="Original Language" fact={spokenLanguage} /> <Fact title={t("jellyseerr.original_language")} fact={spokenLanguage} />
<Facts <Facts
title="Production Country" title={t("jellyseerr.production_country")}
facts={details?.productionCountries?.map((n, idx) => ( facts={details?.productionCountries?.map((n, idx) => (
<View key={idx} className="flex flex-row items-center space-x-2"> <View key={idx} className="flex flex-row items-center space-x-2">
<CountryFlag isoCode={n.iso_3166_1} size={10} /> <CountryFlag isoCode={n.iso_3166_1} size={10} />
@@ -199,14 +201,14 @@ const DetailFacts: React.FC<
))} ))}
/> />
<Facts <Facts
title="Studios" title={t("jellyseerr.studios")}
facts={uniqBy(details?.productionCompanies, "name")?.map( facts={uniqBy(details?.productionCompanies, "name")?.map(
(n) => n.name (n) => n.name
)} )}
/> />
<Facts title="Network" facts={networks?.map((n) => n.name)} /> <Facts title={t("jellyseerr.network")}facts={networks?.map((n) => n.name)} />
<Facts <Facts
title="Currently Streaming on" title={t("jellyseerr.currently_streaming_on")}
facts={streamingProviders?.map((s) => s.name)} facts={streamingProviders?.map((s) => s.name)}
/> />
</View> </View>

View File

@@ -10,6 +10,7 @@ import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/request
import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types"; import {BottomSheetModalMethods} from "@gorhom/bottom-sheet/lib/typescript/types";
import {Button} from "@/components/Button"; import {Button} from "@/components/Button";
import {Text} from "@/components/common/Text"; import {Text} from "@/components/common/Text";
import { useTranslation } from "react-i18next";
interface Props { interface Props {
id: number; id: number;
@@ -36,6 +37,8 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
userId: jellyseerrUser?.id userId: jellyseerrUser?.id
}); });
const { t } = useTranslation();
const [modalRequestProps, setModalRequestProps] = useState<MediaRequestBody>(); const [modalRequestProps, setModalRequestProps] = useState<MediaRequestBody>();
const {data: serviceSettings} = useQuery({ const {data: serviceSettings} = useQuery({
@@ -103,7 +106,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
); );
const seasonTitle = useMemo( const seasonTitle = useMemo(
() => modalRequestProps?.seasons?.length ? `Season (${modalRequestProps?.seasons})` : undefined, () => modalRequestProps?.seasons?.length ? t("jellyseerr.season_x", {seasons: modalRequestProps?.seasons}) : undefined,
[modalRequestProps?.seasons] [modalRequestProps?.seasons]
); );
@@ -148,7 +151,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
return <BottomSheetView> return <BottomSheetView>
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2"> <View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
<View> <View>
<Text className="font-bold text-2xl text-neutral-100">Advanced</Text> <Text className="font-bold text-2xl text-neutral-100">{t("jellyseerr.advanced")}</Text>
{seasonTitle && {seasonTitle &&
<Text className="text-neutral-300">{seasonTitle}</Text> <Text className="text-neutral-300">{seasonTitle}</Text>
} }
@@ -161,27 +164,27 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
titleExtractor={(item) => item.name} titleExtractor={(item) => item.name}
placeholderText={defaultProfile.name} placeholderText={defaultProfile.name}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
label={"Quality Profile"} label={t("jellyseerr.quality_profile")}
onSelected={(item) => onSelected={(item) =>
item && setRequestOverrides((prev) => ({ item && setRequestOverrides((prev) => ({
...prev, ...prev,
profileId: item?.id profileId: item?.id
})) }))
} }
title={"Quality Profile"} title={t("jellyseerr.quality_profile")}
/> />
<Dropdown <Dropdown
data={defaultServiceDetails.rootFolders} data={defaultServiceDetails.rootFolders}
titleExtractor={pathTitleExtractor} titleExtractor={pathTitleExtractor}
placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""} placeholderText={defaultFolder ? pathTitleExtractor(defaultFolder) : ""}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
label={"Root Folder"} label={t("jellyseerr.root_folder")}
onSelected={(item) => onSelected={(item) =>
item && setRequestOverrides((prev) => ({ item && setRequestOverrides((prev) => ({
...prev, ...prev,
rootFolder: item.path rootFolder: item.path
}))} }))}
title={"Root Folder"} title={t("jellyseerr.root_folder")}
/> />
<Dropdown <Dropdown
multi={true} multi={true}
@@ -189,28 +192,28 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
titleExtractor={(item) => item.label} titleExtractor={(item) => item.label}
placeholderText={defaultTags.map(t => t.label).join(",")} placeholderText={defaultTags.map(t => t.label).join(",")}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
label={"Tags"} label={t("jellyseerr.tags")}
onSelected={(...item) => onSelected={(...item) =>
item && setRequestOverrides((prev) => ({ item && setRequestOverrides((prev) => ({
...prev, ...prev,
tags: item.map(i => i.id) tags: item.map(i => i.id)
})) }))
} }
title={"Tags"} title={t("jellyseerr.tags")}
/> />
<Dropdown <Dropdown
data={users} data={users}
titleExtractor={(item) => item.displayName} titleExtractor={(item) => item.displayName}
placeholderText={jellyseerrUser!!.displayName} placeholderText={jellyseerrUser!!.displayName}
keyExtractor={(item) => item.id.toString() || ""} keyExtractor={(item) => item.id.toString() || ""}
label={"Request As"} label={t("jellyseerr.request_as")}
onSelected={(item) => onSelected={(item) =>
item && setRequestOverrides((prev) => ({ item && setRequestOverrides((prev) => ({
...prev, ...prev,
userId: item?.id userId: item?.id
})) }))
} }
title={"Request As"} title={t("jellyseerr.request_as")}
/> />
</> </>
) )
@@ -221,7 +224,7 @@ const RequestModal = forwardRef<BottomSheetModalMethods, Props & Omit<ViewProps,
onPress={request} onPress={request}
color="purple" color="purple"
> >
Request {t("jellyseerr.request_button")}
</Button> </Button>
</View> </View>
</BottomSheetView> </BottomSheetView>

View File

@@ -256,8 +256,8 @@ const JellyseerrSeasons: React.FC<{
<Tags <Tags
textClass="" textClass=""
tags={[ tags={[
`Season ${season.seasonNumber}`, t("jellyseerr.season_number", {season_number: season.seasonNumber}),
`${season.episodeCount} Episodes`, t("jellyseerr.number_episodes", {episode_number: season.episodeCount}),
]} ]}
/> />
{[0].map(() => { {[0].map(() => {

View File

@@ -365,6 +365,9 @@
"none": "None", "none": "None",
"download": { "download": {
"download_season": "Download Season", "download_season": "Download Season",
"download_series": "Download Series",
"download_episode": "Download Episode",
"download_movie": "Download Movie",
"download_x_item": "Download {{item_count}} items", "download_x_item": "Download {{item_count}} items",
"download_button": "Download", "download_button": "Download",
"using_optimized_server": "Using optimized server", "using_optimized_server": "Using optimized server",
@@ -397,6 +400,31 @@
"request_button": "Request", "request_button": "Request",
"are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?", "are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?",
"failed_to_login": "Failed to login", "failed_to_login": "Failed to login",
"cast": "Cast",
"details": "Details",
"status": "Status",
"original_title": "Original Title",
"series_type": "Series Type",
"release_dates": "Release Dates",
"first_air_date": "First Air Date",
"next_air_date": "Next Air Date",
"revenue": "Revenue",
"budget": "Budget",
"original_language": "Original Language",
"production_country": "Production Country",
"studios": "Studios",
"network": "Network",
"currently_streaming_on": "Currently Streaming on",
"advanced": "Advanced",
"request_as": "Request As",
"tags": "Tags",
"quality_profile": "Quality Profile",
"root_folder": "Root Folder",
"season_x": "Season {{seasons}}",
"season_number": "Season {{season_number}}",
"number_episodes": "{{episode_number}} Episodes",
"born": "Born",
"appearances": "Appearances",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0", "jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0",
"jellyseerr_test_failed": "Jellyseerr test failed. Please try again.", "jellyseerr_test_failed": "Jellyseerr test failed. Please try again.",

View File

@@ -365,6 +365,9 @@
"none": "Aucun", "none": "Aucun",
"download": { "download": {
"download_season": "Télécharger la saison", "download_season": "Télécharger la saison",
"download_series": "Télécharger la série",
"download_episode": "Télécharger l'épisode",
"download_movie": "Télécharger le film",
"download_x_item": "Télécharger {{item_count}} items", "download_x_item": "Télécharger {{item_count}} items",
"download_button": "Télécharger", "download_button": "Télécharger",
"using_optimized_server": "Avec le serveur de versions optimisées", "using_optimized_server": "Avec le serveur de versions optimisées",
@@ -397,6 +400,31 @@
"request_button": "Demander", "request_button": "Demander",
"are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?", "are_you_sure_you_want_to_request_all_seasons": "Êtes-vous sûr de vouloir demander toutes les saisons?",
"failed_to_login": "Échec de la connexion", "failed_to_login": "Échec de la connexion",
"cast": "Distribution",
"details": "Détails",
"status": "Statut",
"original_title": "Titre original",
"series_type": "Type de série",
"release_dates": "Dates de sortie",
"first_air_date": "Date de première diffusion",
"next_air_date": "Date de prochaine diffusion",
"revenue": "Revenu",
"budget": "Budget",
"original_language": "Langue originale",
"production_country": "Pays de production",
"studios": "Studios",
"network": "Réseaux",
"currently_streaming_on": "En diffusion continue sur",
"advanced": "Avancé",
"request_as": "Demander en tant que",
"tags": "Tags",
"quality_profile": "Profil de qualité",
"root_folder": "Dossier racine",
"season_x": "Saison {{seasons}}",
"season_number": "Saison {{season_number}}",
"number_episodes": "{{episode_number}} épisodes",
"born": "Né(e) le",
"appearances": "Apparitions",
"toasts": { "toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.", "jellyseer_does_not_meet_requirements": "Jellyseer ne répond pas aux exigences! Veuillez mettre à jour au moins vers la version 2.0.0.",
"jellyseerr_test_failed": "Échec du test de Jellyseerr", "jellyseerr_test_failed": "Échec du test de Jellyseerr",