Files
streamyfin/components/settings/Jellyseerr.tsx
Gauvain 7fc74df0aa refactor(jellyseerr): keep the server version out of the field UI; enforce it at login
The resolver field only needs to find the working URL — the Jellyseerr version requirement is irrelevant there and only polluted the UI.

- jellyseerrProbe: validate reachability + that it's a jellyseerr (no version gate, no version-too-low outcome).

- Drop the version-too-low reason from the whole resolver stack (types, resolve, hook, status text, i18n).

- Min version 2.0.0 stays enforced in JellyseerrApi.test() at login: now writes an error log + toast, and uses numeric isVersionBelow (fixes the "2.10.0" < "2.0.0" string-compare bug).
2026-06-04 21:24:40 +02:00

194 lines
7.0 KiB
TypeScript

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