forked from Ninjalama/streamyfin_mirror
Compare commits
2 Commits
refactor/b
...
fix/video-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeaa027579 | ||
|
|
63965c9e64 |
@@ -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, Ionicons } from "@expo/vector-icons";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
|
||||
@@ -45,18 +45,6 @@ export default function IndexLayout() {
|
||||
name="settings"
|
||||
options={{
|
||||
title: "Settings",
|
||||
headerRight: () => (
|
||||
<View className="">
|
||||
<Ionicons
|
||||
name="file-tray-full-outline"
|
||||
size={22}
|
||||
color="white"
|
||||
onPress={() => {
|
||||
router.push("/logs");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (
|
||||
|
||||
@@ -20,6 +20,12 @@ 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 = () => {
|
||||
@@ -123,6 +129,30 @@ export default function settings() {
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-bold text-lg mb-2">Logs</Text>
|
||||
<View className="flex flex-col space-y-2">
|
||||
{logs?.map((log, index) => (
|
||||
<View key={index} className="bg-neutral-900 rounded-xl p-3">
|
||||
<Text
|
||||
className={`
|
||||
mb-1
|
||||
${log.level === "INFO" && "text-blue-500"}
|
||||
${log.level === "ERROR" && "text-red-500"}
|
||||
`}
|
||||
>
|
||||
{log.level}
|
||||
</Text>
|
||||
<Text uiTextView selectable className="text-xs">
|
||||
{log.message}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{logs?.length === 0 && (
|
||||
<Text className="opacity-50">No logs available</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Controls } from "@/components/video-player/Controls";
|
||||
import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar";
|
||||
import { useOrientation } from "@/hooks/useOrientation";
|
||||
import { useOrientationSettings } from "@/hooks/useOrientationSettings";
|
||||
import useScreenDimensions from "@/hooks/useScreenDimensions";
|
||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
@@ -17,7 +18,13 @@ import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Dimensions, Pressable, StatusBar, View } from "react-native";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import Video, {
|
||||
@@ -34,8 +41,7 @@ export default function page() {
|
||||
const poster = usePoster(playSettings, api);
|
||||
const videoSource = useVideoSource(playSettings, api, poster, playUrl);
|
||||
const firstTime = useRef(true);
|
||||
|
||||
const screenDimensions = Dimensions.get("screen");
|
||||
const screenDimensions = useScreenDimensions();
|
||||
|
||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
|
||||
@@ -345,13 +345,6 @@ function Layout() {
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="logs"
|
||||
options={{
|
||||
presentation: "modal",
|
||||
title: "Logs",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/play-offline-video"
|
||||
options={{
|
||||
|
||||
107
app/login.tsx
107
app/login.tsx
@@ -2,12 +2,11 @@ import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { getSystemApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
@@ -28,7 +27,6 @@ const Login: React.FC = () => {
|
||||
const { setServer, login, removeServer, initiateQuickConnect } =
|
||||
useJellyfin();
|
||||
const [api] = useAtom(apiAtom);
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams();
|
||||
|
||||
const {
|
||||
@@ -74,17 +72,7 @@ const Login: React.FC = () => {
|
||||
try {
|
||||
const result = CredentialsSchema.safeParse(credentials);
|
||||
if (result.success) {
|
||||
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");
|
||||
await login(credentials.username, credentials.password);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -117,72 +105,37 @@ 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 timeout = 5000; // 5 seconds timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
const protocols = ["https://", "http://"];
|
||||
const timeout = 2000; // 2 seconds timeout for long 404 responses
|
||||
|
||||
try {
|
||||
// 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);
|
||||
}
|
||||
for (const protocol of protocols) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// 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}`
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@@ -244,16 +197,6 @@ const Login: React.FC = () => {
|
||||
style={{ flex: 1, height: "100%" }}
|
||||
>
|
||||
<View className="flex flex-col w-full h-full relative items-center justify-center">
|
||||
<View className="absolute top-4 right-4">
|
||||
<Ionicons
|
||||
name="file-tray-full-outline"
|
||||
size={22}
|
||||
color="white"
|
||||
onPress={() => {
|
||||
router.push("/logs");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View className="px-4 -mt-20">
|
||||
<View className="mb-4">
|
||||
<Text className="text-3xl font-bold mb-1">
|
||||
|
||||
58
app/logs.tsx
58
app/logs.tsx
@@ -1,58 +0,0 @@
|
||||
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 (
|
||||
<ScrollView
|
||||
className="flex-1 p-4"
|
||||
contentContainerStyle={{ gap: 10, paddingBottom: insets.top }}
|
||||
>
|
||||
<View className="flex flex-col">
|
||||
{logs?.map((log, index) => (
|
||||
<View key={index} className="border-b-neutral-800 border py-3">
|
||||
<View className="flex flex-row justify-between items-center mb-2">
|
||||
<Text
|
||||
className={`
|
||||
text-xs
|
||||
${log.level === "INFO" && "text-blue-500"}
|
||||
${log.level === "ERROR" && "text-red-500"}
|
||||
`}
|
||||
>
|
||||
{log.level}
|
||||
</Text>
|
||||
<Text className="text-xs text-neutral-500">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</Text>
|
||||
</View>
|
||||
<Text uiTextView selectable className="text-xs mb-1">
|
||||
{log.message}
|
||||
</Text>
|
||||
{log.data && (
|
||||
<Text uiTextView selectable className="text-xs">
|
||||
{log.data}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
{logs?.length === 0 && (
|
||||
<Text className="opacity-50">No logs available</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
27
hooks/useScreenDimensions.ts
Normal file
27
hooks/useScreenDimensions.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Dimensions, ScaledSize } from "react-native";
|
||||
|
||||
const useScreenDimensions = (): ScaledSize => {
|
||||
const [screenDimensions, setScreenDimensions] = useState(
|
||||
Dimensions.get("screen")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
setScreenDimensions(Dimensions.get("screen"));
|
||||
};
|
||||
|
||||
const dimensionsListener = Dimensions.addEventListener(
|
||||
"change",
|
||||
updateDimensions
|
||||
);
|
||||
|
||||
return () => {
|
||||
dimensionsListener.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return screenDimensions;
|
||||
};
|
||||
|
||||
export default useScreenDimensions;
|
||||
@@ -1,5 +1,4 @@
|
||||
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";
|
||||
@@ -213,35 +212,20 @@ 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?"
|
||||
);
|
||||
@@ -328,9 +312,6 @@ 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");
|
||||
|
||||
@@ -29,7 +29,7 @@ export const writeToLog = async (
|
||||
const logs: LogEntry[] = currentLogs ? JSON.parse(currentLogs) : [];
|
||||
logs.push(newEntry);
|
||||
|
||||
const maxLogs = 1000;
|
||||
const maxLogs = 100;
|
||||
const recentLogs = logs.slice(Math.max(logs.length - maxLogs, 0));
|
||||
|
||||
await AsyncStorage.setItem("logs", JSON.stringify(recentLogs));
|
||||
|
||||
Reference in New Issue
Block a user