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));