fix: improved login flow for jellyseerr

This commit is contained in:
Fredrik Burmester
2024-12-31 10:29:50 +01:00
parent 47211ba009
commit 36a689f59d
3 changed files with 213 additions and 152 deletions

View File

@@ -47,7 +47,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
<TouchableOpacity
className={`
p-3 rounded-xl items-center justify-center
${loading || (disabled && "opacity-50")}
${(loading || disabled) && "opacity-50"}
${colorClasses}
${className}
`}

View File

@@ -0,0 +1,207 @@
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
import { View } from "react-native";
import { Text } from "../common/Text";
import { useCallback, useRef, useState } from "react";
import { Input } from "../common/Input";
import { ListItem } from "../ListItem";
import { Loader } from "../Loader";
import { useSettings } from "@/utils/atoms/settings";
import { Button } from "../Button";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useAtom } from "jotai";
import { toast } from "sonner-native";
import { useMutation } from "@tanstack/react-query";
export const JellyseerrSettings = () => {
const {
jellyseerrApi,
jellyseerrUser,
setJellyseerrUser,
clearAllJellyseerData,
} = useJellyseerr();
const [user] = useAtom(userAtom);
const [settings, updateSettings] = useSettings();
const [promptForJellyseerrPass, setPromptForJellyseerrPass] =
useState<boolean>(false);
const [jellyseerrPassword, setJellyseerrPassword] = useState<
string | undefined
>(undefined);
const [jellyseerrServerUrl, setjellyseerrServerUrl] = useState<
string | undefined
>(settings?.jellyseerrServerUrl || undefined);
const loginToJellyseerrMutation = useMutation({
mutationFn: async () => {
if (!jellyseerrServerUrl || !user?.Name || !jellyseerrPassword) {
throw new Error("Missing required information for login");
}
const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl);
return jellyseerrTempApi.login(user.Name, jellyseerrPassword);
},
onSuccess: (user) => {
setJellyseerrUser(user);
updateSettings({ jellyseerrServerUrl });
},
onError: () => {
toast.error("Failed to login");
},
onSettled: () => {
setJellyseerrPassword(undefined);
},
});
const testJellyseerrServerUrlMutation = useMutation({
mutationFn: async () => {
if (!jellyseerrServerUrl || jellyseerrApi) return null;
const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl);
return jellyseerrTempApi.test();
},
onSuccess: (result) => {
if (result && result.isValid) {
if (result.requiresPass) {
setPromptForJellyseerrPass(true);
} else {
updateSettings({ jellyseerrServerUrl });
}
} else {
setPromptForJellyseerrPass(false);
setjellyseerrServerUrl(undefined);
clearAllJellyseerData();
}
},
});
const clearData = () => {
clearAllJellyseerData().finally(() => {
setjellyseerrServerUrl(undefined);
setPromptForJellyseerrPass(false);
});
};
return (
<View className="mt-4">
<Text className="text-lg font-bold mb-2">Jellyseerr</Text>
<View>
{jellyseerrUser ? (
<View className="flex flex-col rounded-xl overflow-hidden bg-neutral-900 pt-0 divide-y divide-neutral-800">
<ListItem
title="Total media requests"
subTitle={jellyseerrUser?.requestCount?.toString()}
/>
<ListItem
title="Movie quota limit"
subTitle={
jellyseerrUser?.movieQuotaLimit?.toString() ?? "Unlimited"
}
/>
<ListItem
title="Movie quota days"
subTitle={
jellyseerrUser?.movieQuotaDays?.toString() ?? "Unlimited"
}
/>
<ListItem
title="TV quota limit"
subTitle={jellyseerrUser?.tvQuotaLimit?.toString() ?? "Unlimited"}
/>
<ListItem
title="TV quota days"
subTitle={jellyseerrUser?.tvQuotaDays?.toString() ?? "Unlimited"}
/>
<View className="p-4">
<Button color="red" onPress={clearData}>
Reset Jellyseerr config
</Button>
</View>
</View>
) : (
<View className="flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900">
<Text className="text-xs text-red-600 mb-2">
This integration is in its early stages. Expect things to change.
</Text>
<Text className="font-bold mb-1">Server URL</Text>
<View className="flex flex-col shrink mb-2">
<Text className="text-xs text-gray-600">
Example: http(s)://your-host.url
</Text>
<Text className="text-xs text-gray-600">
(add port if required)
</Text>
</View>
<Input
placeholder="Jellyseerr URL..."
value={settings?.jellyseerrServerUrl ?? jellyseerrServerUrl}
defaultValue={
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
}
keyboardType="url"
returnKeyType="done"
autoCapitalize="none"
textContentType="URL"
onChangeText={setjellyseerrServerUrl}
editable={!testJellyseerrServerUrlMutation.isPending}
/>
<Button
loading={testJellyseerrServerUrlMutation.isPending}
disabled={testJellyseerrServerUrlMutation.isPending}
color={promptForJellyseerrPass ? "red" : "purple"}
className="h-12 mt-2"
onPress={() => {
if (promptForJellyseerrPass) {
clearData();
return;
}
testJellyseerrServerUrlMutation.mutate();
}}
style={{
marginBottom: 8,
}}
>
{promptForJellyseerrPass ? "Clear" : "Save"}
</Button>
<View
pointerEvents={promptForJellyseerrPass ? "auto" : "none"}
style={{
opacity: promptForJellyseerrPass ? 1 : 0.5,
}}
>
<Text className="font-bold mb-2">Password</Text>
<Input
autoFocus={true}
focusable={true}
placeholder={`Enter password for Jellyfin user ${user?.Name}`}
value={jellyseerrPassword}
keyboardType="default"
secureTextEntry={true}
returnKeyType="done"
autoCapitalize="none"
textContentType="password"
onChangeText={setJellyseerrPassword}
editable={
!loginToJellyseerrMutation.isPending &&
promptForJellyseerrPass
}
/>
<Button
loading={loginToJellyseerrMutation.isPending}
disabled={loginToJellyseerrMutation.isPending}
color="purple"
className="h-12 mt-2"
onPress={() => loginToJellyseerrMutation.mutate()}
>
Login
</Button>
</View>
</View>
)}
</View>
</View>
);
};

View File

@@ -21,7 +21,7 @@ import * as BackgroundFetch from "expo-background-fetch";
import * as ScreenOrientation from "expo-screen-orientation";
import * as TaskManager from "expo-task-manager";
import { useAtom } from "jotai";
import React, {useCallback, useEffect, useRef, useState} from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Linking,
Switch,
@@ -40,35 +40,23 @@ import { Stepper } from "@/components/inputs/Stepper";
import { MediaProvider } from "./MediaContext";
import { SubtitleToggles } from "./SubtitleToggles";
import { AudioToggles } from "./AudioToggles";
import {JellyseerrApi, useJellyseerr} from "@/hooks/useJellyseerr";
import {ListItem} from "@/components/ListItem";
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
import { ListItem } from "@/components/ListItem";
import { JellyseerrSettings } from "./Jellyseerr";
interface Props extends ViewProps {}
export const SettingToggles: React.FC<Props> = ({ ...props }) => {
const [settings, updateSettings] = useSettings();
const { setProcesses } = useDownload();
const {
jellyseerrApi,
jellyseerrUser,
setJellyseerrUser ,
clearAllJellyseerData
} = useJellyseerr();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const jellyseerrPassInputRef = useRef(null);
const [marlinUrl, setMarlinUrl] = useState<string>("");
const [promptForJellyseerrPass, setPromptForJellyseerrPass] = useState<boolean>(false);
const [isJellyseerrLoading, setIsLoadingJellyseerr] = useState<boolean>(false);
const [jellyseerrPassword, setJellyseerrPassword] = useState<string | undefined>(undefined);
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
useState<string>(settings?.optimizedVersionsServerUrl || "");
const [jellyseerrServerUrl, setjellyseerrServerUrl] =
useState<string | undefined>(settings?.jellyseerrServerUrl || undefined);
const queryClient = useQueryClient();
/********************
@@ -123,49 +111,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
staleTime: 0,
});
const loginToJellyseerr = useCallback(() => {
if (jellyseerrServerUrl && user?.Name && jellyseerrPassword) {
setIsLoadingJellyseerr(true)
const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl);
jellyseerrTempApi.login(user?.Name, jellyseerrPassword)
.then(user => {
setJellyseerrUser(user);
updateSettings({jellyseerrServerUrl})
})
.catch(() => {
toast.error("Failed to login to jellyseerr!")
})
.finally(() => {
setJellyseerrPassword(undefined);
setPromptForJellyseerrPass(false)
setIsLoadingJellyseerr(false)
})
}
}, [user, jellyseerrServerUrl, jellyseerrPassword]);
const testJellyseerrServerUrl = useCallback(async () => {
if (!jellyseerrServerUrl || jellyseerrApi)
return;
setIsLoadingJellyseerr(true)
const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl);
jellyseerrTempApi.test().then(result => {
if (result.isValid) {
if (result.requiresPass)
setPromptForJellyseerrPass(true)
// promptForJellyseerrLogin()
else
updateSettings({jellyseerrServerUrl})
}
else {
setPromptForJellyseerrPass(false)
setjellyseerrServerUrl(undefined);
clearAllJellyseerData();
}
}).finally(() => setIsLoadingJellyseerr(false))
}, [jellyseerrServerUrl])
if (!settings) return null;
return (
@@ -692,98 +637,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</View>
</View>
<View className="mt-4">
<Text className="text-lg font-bold mb-2">Jellyseerr</Text>
<View className="flex flex-col rounded-xl overflow-hidden divide-y-2 divide-solid divide-neutral-800">
{jellyseerrUser && <>
<View className="flex flex-col overflow-hidden divide-y-2 divide-solid divide-neutral-800">
<ListItem title="Total media requests" subTitle={jellyseerrUser?.requestCount?.toString()}/>
<ListItem title="Movie quota limit" subTitle={jellyseerrUser?.movieQuotaLimit?.toString() ?? "Unlimited"}/>
<ListItem title="Movie quota days" subTitle={jellyseerrUser?.movieQuotaDays?.toString() ?? "Unlimited"}/>
<ListItem title="TV quota limit" subTitle={jellyseerrUser?.tvQuotaLimit?.toString() ?? "Unlimited"}/>
<ListItem title="TV quota days" subTitle={jellyseerrUser?.tvQuotaDays?.toString() ?? "Unlimited"}/>
</View>
</>}
<View
pointerEvents={
!jellyseerrUser || jellyseerrPassword ? "auto" : "none"
}
className={`
${
!jellyseerrUser || jellyseerrPassword
? "opacity-100"
: "opacity-50"
}`}
>
<View className="flex flex-col bg-neutral-900 px-4 py-4 space-y-2">
<View className="flex flex-col shrink mb-2">
<View className="flex flex-row justify-between items-center">
<Text className="font-semibold">
Server URL
</Text>
</View>
<Text className="text-xs opacity-50">
Set the URL for your jellyseerr instance.
</Text>
<Text className="text-xs text-gray-600">Example: http(s)://your-host.url</Text>
<Text className="text-xs text-gray-600 mb-1">(add port if required)</Text>
<Text className="text-xs text-red-600">This integration is in its early stages. Expect things to change.</Text>
</View>
<Input
placeholder="Jellyseerrs server URL..."
value={settings?.jellyseerrServerUrl ?? jellyseerrServerUrl}
keyboardType="url"
returnKeyType="done"
autoCapitalize="none"
textContentType="URL"
onChangeText={setjellyseerrServerUrl}
/>
{promptForJellyseerrPass &&
<Input
autoFocus={true}
focusable={true}
placeholder={`Enter password for jellyfin user ${user?.Name}`}
value={jellyseerrPassword}
keyboardType="default"
secureTextEntry={true}
returnKeyType="done"
autoCapitalize="none"
textContentType="password"
onChangeText={setJellyseerrPassword}
/>
}
</View>
</View>
{isJellyseerrLoading &&
<Loader className="rounded-xl overflow-hidden w-full h-full bg-neutral-700/10 absolute"/>
}
<Button
color="purple"
className="h-12 mt-2"
onPress={() =>
jellyseerrUser
? clearAllJellyseerData().finally(() => {
setjellyseerrServerUrl(undefined)
setPromptForJellyseerrPass(false)
setIsLoadingJellyseerr(false)
})
:
promptForJellyseerrPass
? loginToJellyseerr()
: testJellyseerrServerUrl()
}
>
{jellyseerrUser
? "Clear jellyseerr data"
:
promptForJellyseerrPass
? "Login"
: "Save"
}
</Button>
</View>
</View>
<JellyseerrSettings />
</View>
);
};