mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-18 03:40:23 +01:00
fix: improved login flow for jellyseerr
This commit is contained in:
@@ -47,7 +47,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className={`
|
className={`
|
||||||
p-3 rounded-xl items-center justify-center
|
p-3 rounded-xl items-center justify-center
|
||||||
${loading || (disabled && "opacity-50")}
|
${(loading || disabled) && "opacity-50"}
|
||||||
${colorClasses}
|
${colorClasses}
|
||||||
${className}
|
${className}
|
||||||
`}
|
`}
|
||||||
|
|||||||
207
components/settings/Jellyseerr.tsx
Normal file
207
components/settings/Jellyseerr.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,7 +21,7 @@ import * as BackgroundFetch from "expo-background-fetch";
|
|||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
import * as TaskManager from "expo-task-manager";
|
import * as TaskManager from "expo-task-manager";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, {useCallback, useEffect, useRef, useState} from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -40,35 +40,23 @@ import { Stepper } from "@/components/inputs/Stepper";
|
|||||||
import { MediaProvider } from "./MediaContext";
|
import { MediaProvider } from "./MediaContext";
|
||||||
import { SubtitleToggles } from "./SubtitleToggles";
|
import { SubtitleToggles } from "./SubtitleToggles";
|
||||||
import { AudioToggles } from "./AudioToggles";
|
import { AudioToggles } from "./AudioToggles";
|
||||||
import {JellyseerrApi, useJellyseerr} from "@/hooks/useJellyseerr";
|
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import {ListItem} from "@/components/ListItem";
|
import { ListItem } from "@/components/ListItem";
|
||||||
|
import { JellyseerrSettings } from "./Jellyseerr";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
||||||
const [settings, updateSettings] = useSettings();
|
const [settings, updateSettings] = useSettings();
|
||||||
const { setProcesses } = useDownload();
|
const { setProcesses } = useDownload();
|
||||||
const {
|
|
||||||
jellyseerrApi,
|
|
||||||
jellyseerrUser,
|
|
||||||
setJellyseerrUser ,
|
|
||||||
clearAllJellyseerData
|
|
||||||
} = useJellyseerr();
|
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
|
||||||
const jellyseerrPassInputRef = useRef(null);
|
|
||||||
const [marlinUrl, setMarlinUrl] = useState<string>("");
|
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] =
|
const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] =
|
||||||
useState<string>(settings?.optimizedVersionsServerUrl || "");
|
useState<string>(settings?.optimizedVersionsServerUrl || "");
|
||||||
|
|
||||||
const [jellyseerrServerUrl, setjellyseerrServerUrl] =
|
|
||||||
useState<string | undefined>(settings?.jellyseerrServerUrl || undefined);
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
/********************
|
/********************
|
||||||
@@ -123,49 +111,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
staleTime: 0,
|
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;
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -692,98 +637,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="mt-4">
|
<JellyseerrSettings />
|
||||||
<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>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user