From 6752888bb0fb7b56b8ffd3d72e9fac3f597c8831 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 11 Oct 2024 16:42:13 +0200 Subject: [PATCH] wip --- app/(auth)/(tabs)/(home)/_layout.tsx | 14 +++- app/(auth)/(tabs)/(home)/settings.tsx | 30 ------- app/_layout.tsx | 7 ++ app/login.tsx | 109 ++++++++++++++++++++------ app/logs.tsx | 58 ++++++++++++++ providers/JellyfinProvider.tsx | 19 +++++ utils/log.ts | 2 +- 7 files changed, 181 insertions(+), 58 deletions(-) create mode 100644 app/logs.tsx diff --git a/app/(auth)/(tabs)/(home)/_layout.tsx b/app/(auth)/(tabs)/(home)/_layout.tsx index f51ecbf5..67e8f1a7 100644 --- a/app/(auth)/(tabs)/(home)/_layout.tsx +++ b/app/(auth)/(tabs)/(home)/_layout.tsx @@ -2,7 +2,7 @@ import { Chromecast } from "@/components/Chromecast"; import { HeaderBackButton } from "@/components/common/HeaderBackButton"; import { nestedTabPageScreenOptions } from "@/components/stacks/NestedTabPageStack"; import { useDownload } from "@/providers/DownloadProvider"; -import { Feather } from "@expo/vector-icons"; +import { Feather, Ionicons } from "@expo/vector-icons"; import { Stack, useRouter } from "expo-router"; import { Platform, TouchableOpacity, View } from "react-native"; @@ -45,6 +45,18 @@ export default function IndexLayout() { name="settings" options={{ title: "Settings", + headerRight: () => ( + + { + router.push("/logs"); + }} + /> + + ), }} /> {Object.entries(nestedTabPageScreenOptions).map(([name, options]) => ( diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 6c5cc32a..f1e1177d 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -20,12 +20,6 @@ export default function settings() { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const { data: logs } = useQuery({ - queryKey: ["logs"], - queryFn: async () => readFromLog(), - refetchInterval: 1000, - }); - const insets = useSafeAreaInsets(); const openQuickConnectAuthCodeInput = () => { @@ -129,30 +123,6 @@ export default function settings() { - - Logs - - {logs?.map((log, index) => ( - - - {log.level} - - - {log.message} - - - ))} - {logs?.length === 0 && ( - No logs available - )} - - ); diff --git a/app/_layout.tsx b/app/_layout.tsx index 9c32fc76..376bf711 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -345,6 +345,13 @@ function Layout() { animation: "fade", }} /> + { const { setServer, login, removeServer, initiateQuickConnect } = useJellyfin(); const [api] = useAtom(apiAtom); + const router = useRouter(); const params = useLocalSearchParams(); const { @@ -72,7 +74,17 @@ const Login: React.FC = () => { try { const result = CredentialsSchema.safeParse(credentials); if (result.success) { - await login(credentials.username, credentials.password); + try { + await login(credentials.username, credentials.password); + } catch (loginError) { + if (loginError instanceof Error) { + setError(loginError.message); + } else { + setError("An unexpected error occurred during login"); + } + } + } else { + setError("Invalid credentials format"); } } catch (error) { if (error instanceof Error) { @@ -105,37 +117,72 @@ const Login: React.FC = () => { async function checkUrl(url: string) { url = url.endsWith("/") ? url.slice(0, -1) : url; setLoadingServerCheck(true); + writeToLog("INFO", `Checking URL: ${url}`); - const protocols = ["https://", "http://"]; - const timeout = 2000; // 2 seconds timeout for long 404 responses + const timeout = 5000; // 5 seconds timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); try { - for (const protocol of protocols) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - try { - const response = await fetch(`${protocol}${url}/System/Info/Public`, { - mode: "cors", - signal: controller.signal, - }); - clearTimeout(timeoutId); - if (response.ok) { - const data = (await response.json()) as PublicSystemInfo; - setServerName(data.ServerName || ""); - return `${protocol}${url}`; - } - } catch (e) { - const error = e as Error; - if (error.name === "AbortError") { - console.log(`Request to ${protocol}${url} timed out`); - } else { - console.log(`Error checking ${protocol}${url}:`, error); - } + // Try HTTPS first + const httpsUrl = `https://${url}/System/Info/Public`; + try { + const response = await fetch(httpsUrl, { + mode: "cors", + signal: controller.signal, + }); + if (response.ok) { + const data = (await response.json()) as PublicSystemInfo; + setServerName(data.ServerName || ""); + return `https://${url}`; + } else { + writeToLog( + "WARN", + `HTTPS connection failed with status: ${response.status}` + ); } + } catch (e) { + writeToLog("WARN", "HTTPS connection failed - trying HTTP", e); + } + + // If HTTPS didn't work, try HTTP + const httpUrl = `http://${url}/System/Info/Public`; + try { + const response = await fetch(httpUrl, { + mode: "cors", + signal: controller.signal, + }); + writeToLog("INFO", `HTTP response status: ${response.status}`); + if (response.ok) { + const data = (await response.json()) as PublicSystemInfo; + setServerName(data.ServerName || ""); + return `http://${url}`; + } else { + writeToLog( + "WARN", + `HTTP connection failed with status: ${response.status}` + ); + } + } catch (e) { + writeToLog("ERROR", "HTTP connection failed", e); + } + + // If neither worked, return undefined + writeToLog( + "ERROR", + `Failed to connect to ${url} using both HTTPS and HTTP` + ); + return undefined; + } catch (e) { + const error = e as Error; + if (error.name === "AbortError") { + writeToLog("ERROR", `Request to ${url} timed out`, error); + } else { + writeToLog("ERROR", `Unexpected error checking ${url}`, error); } return undefined; } finally { + clearTimeout(timeoutId); setLoadingServerCheck(false); } } @@ -197,6 +244,16 @@ const Login: React.FC = () => { style={{ flex: 1, height: "100%" }} > + + { + router.push("/logs"); + }} + /> + diff --git a/app/logs.tsx b/app/logs.tsx new file mode 100644 index 00000000..0e10df40 --- /dev/null +++ b/app/logs.tsx @@ -0,0 +1,58 @@ +import { Text } from "@/components/common/Text"; +import { readFromLog } from "@/utils/log"; +import { useQuery } from "@tanstack/react-query"; +import { ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +const Logs: React.FC = () => { + const { data: logs } = useQuery({ + queryKey: ["logs"], + queryFn: async () => (await readFromLog()).reverse(), + refetchOnReconnect: true, + refetchOnWindowFocus: true, + refetchOnMount: true, + }); + + const insets = useSafeAreaInsets(); + + return ( + + + {logs?.map((log, index) => ( + + + + {log.level} + + + {new Date(log.timestamp).toLocaleString()} + + + + {log.message} + + {log.data && ( + + {log.data} + + )} + + ))} + {logs?.length === 0 && ( + No logs available + )} + + + ); +}; + +export default Logs; diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index b1e5dda7..015c2e8f 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -1,4 +1,5 @@ import { useInterval } from "@/hooks/useInterval"; +import { writeToLog } from "@/utils/log"; import { Api, Jellyfin } from "@jellyfin/sdk"; import { UserDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getUserApi } from "@jellyfin/sdk/lib/utils/api"; @@ -212,20 +213,35 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ if (axios.isAxiosError(error)) { switch (error.response?.status) { case 401: + writeToLog("ERROR", "Invalid username or password"); throw new Error("Invalid username or password"); case 403: + writeToLog("ERROR", "User does not have permission to log in"); throw new Error("User does not have permission to log in"); case 408: + writeToLog( + "WARN", + "Server is taking too long to respond, try again later" + ); throw new Error( "Server is taking too long to respond, try again later" ); case 429: + writeToLog( + "WARN", + "Server received too many requests, try again later" + ); throw new Error( "Server received too many requests, try again later" ); case 500: + writeToLog("ERROR", "There is a server error"); throw new Error("There is a server error"); default: + writeToLog( + "ERROR", + "An unexpected error occurred. Did you enter the server URL correctly?" + ); throw new Error( "An unexpected error occurred. Did you enter the server URL correctly?" ); @@ -312,6 +328,9 @@ function useProtectedRoute(user: UserDto | null, loading = false) { if (loading) return; const inAuthGroup = segments[0] === "(auth)"; + const inLogs = segments[0] === "logs"; + + if (inLogs) return; if (!user?.Id && inAuthGroup) { router.replace("/login"); diff --git a/utils/log.ts b/utils/log.ts index 0f8af54a..33aca8a7 100644 --- a/utils/log.ts +++ b/utils/log.ts @@ -29,7 +29,7 @@ export const writeToLog = async ( const logs: LogEntry[] = currentLogs ? JSON.parse(currentLogs) : []; logs.push(newEntry); - const maxLogs = 100; + const maxLogs = 1000; const recentLogs = logs.slice(Math.max(logs.length - maxLogs, 0)); await AsyncStorage.setItem("logs", JSON.stringify(recentLogs));