mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-28 14:08:29 +00:00
@@ -3,10 +3,9 @@ import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { AxiosError } from "axios";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
@@ -22,7 +21,8 @@ const CredentialsSchema = z.object({
|
||||
});
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { setServer, login, removeServer } = useJellyfin();
|
||||
const { setServer, login, removeServer, initiateQuickConnect } =
|
||||
useJellyfin();
|
||||
const [api] = useAtom(apiAtom);
|
||||
const params = useLocalSearchParams();
|
||||
|
||||
@@ -87,6 +87,21 @@ const Login: React.FC = () => {
|
||||
setServer({ address: url.trim() });
|
||||
};
|
||||
|
||||
const handleQuickConnect = async () => {
|
||||
try {
|
||||
const code = await initiateQuickConnect();
|
||||
if (code) {
|
||||
Alert.alert("Quick Connect", `Enter code ${code} to login`, [
|
||||
{
|
||||
text: "Got It",
|
||||
},
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert("Error", "Failed to initiate Quick Connect");
|
||||
}
|
||||
};
|
||||
|
||||
if (api?.basePath) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
@@ -162,13 +177,18 @@ const Login: React.FC = () => {
|
||||
<Text className="text-red-600 mb-2">{error}</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
onPress={handleLogin}
|
||||
loading={loading}
|
||||
className="mt-auto mb-2"
|
||||
>
|
||||
Log in
|
||||
</Button>
|
||||
<View className="mt-auto mb-2">
|
||||
<Button
|
||||
color="black"
|
||||
onPress={handleQuickConnect}
|
||||
className="mb-2"
|
||||
>
|
||||
Use Quick Connect
|
||||
</Button>
|
||||
<Button onPress={handleLogin} loading={loading}>
|
||||
Log in
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
|
||||
19
hooks/useInterval.ts
Normal file
19
hooks/useInterval.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useInterval(callback: () => void, delay: number | null) {
|
||||
const savedCallback = useRef<() => void>();
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current?.();
|
||||
}
|
||||
if (delay !== null) {
|
||||
const id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useInterval } from "@/hooks/useInterval";
|
||||
import { Api, Jellyfin } from "@jellyfin/sdk";
|
||||
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { router, useSegments } from "expo-router";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Platform } from "react-native";
|
||||
@@ -29,6 +32,7 @@ interface JellyfinContextValue {
|
||||
removeServer: () => void;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
initiateQuickConnect: () => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
const JellyfinContext = createContext<JellyfinContextValue | undefined>(
|
||||
@@ -51,7 +55,6 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
}) => {
|
||||
const [jellyfin, setJellyfin] = useState<Jellyfin | undefined>(undefined);
|
||||
const [deviceId, setDeviceId] = useState<string | undefined>(undefined);
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -69,6 +72,88 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
|
||||
const [api, setApi] = useAtom(apiAtom);
|
||||
const [user, setUser] = useAtom(userAtom);
|
||||
const [isPolling, setIsPolling] = useState<boolean>(false);
|
||||
const [secret, setSecret] = useState<string | null>(null);
|
||||
|
||||
const headers = useMemo(() => {
|
||||
if (!deviceId) return {};
|
||||
return {
|
||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||
Platform.OS === "android" ? "Android" : "iOS"
|
||||
}, DeviceId="${deviceId}", Version="0.8.4"`,
|
||||
};
|
||||
}, [deviceId]);
|
||||
|
||||
const initiateQuickConnect = useCallback(async () => {
|
||||
if (!api || !deviceId) return;
|
||||
try {
|
||||
const response = await api.axiosInstance.post(
|
||||
api.basePath + "/QuickConnect/Initiate",
|
||||
null,
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
if (response?.status === 200) {
|
||||
setSecret(response?.data?.Secret);
|
||||
setIsPolling(true);
|
||||
console.log("Initiating quick connect");
|
||||
return response.data?.Code;
|
||||
} else {
|
||||
throw new Error("Failed to initiate quick connect");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}, [api, deviceId, headers]);
|
||||
|
||||
const pollQuickConnect = useCallback(async () => {
|
||||
if (!api || !secret) return;
|
||||
|
||||
try {
|
||||
const response = await api.axiosInstance.get(
|
||||
`${api.basePath}/QuickConnect/Connect?Secret=${secret}`
|
||||
);
|
||||
|
||||
console.log("Polling quick connect");
|
||||
if (response.status === 200) {
|
||||
if (response.data.Authenticated) {
|
||||
setIsPolling(false);
|
||||
|
||||
const authResponse = await api.axiosInstance.post(
|
||||
api.basePath + "/Users/AuthenticateWithQuickConnect",
|
||||
{
|
||||
secret,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
const { AccessToken, User } = authResponse.data;
|
||||
api.accessToken = AccessToken;
|
||||
console.log("Quick connect authenticated", AccessToken, User.Id);
|
||||
setUser(User);
|
||||
await AsyncStorage.setItem("token", AccessToken);
|
||||
await AsyncStorage.setItem("user", JSON.stringify(User));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response?.status === 400) {
|
||||
setIsPolling(false);
|
||||
setSecret(null);
|
||||
throw new Error("The code has expired. Please try again.");
|
||||
} else {
|
||||
console.error("Error polling Quick Connect:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}, [api, secret, headers]);
|
||||
|
||||
useInterval(pollQuickConnect, isPolling ? 1000 : null);
|
||||
|
||||
const discoverServers = async (url: string): Promise<Server[]> => {
|
||||
const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
|
||||
@@ -199,6 +284,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
login: (username, password) =>
|
||||
loginMutation.mutateAsync({ username, password }),
|
||||
logout: () => logoutMutation.mutateAsync(),
|
||||
initiateQuickConnect,
|
||||
};
|
||||
|
||||
useProtectedRoute(user, isLoading || isFetching);
|
||||
|
||||
Reference in New Issue
Block a user