Compare commits

...

15 Commits

Author SHA1 Message Date
Fredrik Burmester
6752888bb0 wip 2024-10-11 16:42:13 +02:00
Fredrik Burmester
b73a33b05b chore 2024-10-10 17:27:21 +02:00
Fredrik Burmester
e3baa2f58b fix: rotation issues 2024-10-10 17:27:17 +02:00
Fredrik Burmester
ef7fbc985f chore 2024-10-10 10:10:24 +02:00
Fredrik Burmester
381c6701f2 chore: version bump 2024-10-10 07:56:23 +02:00
Fredrik Burmester
71da79ee6a chore 2024-10-09 20:23:53 +02:00
Fredrik Burmester
5cff323871 feat: go to next episode on end 2024-10-09 20:23:50 +02:00
Fredrik Burmester
39b7c66d34 fix: don't crash app when no media source found for unmatched items 2024-10-09 20:23:40 +02:00
Fredrik Burmester
0a098bf26e chore 2024-10-09 07:28:53 +02:00
Fredrik Burmester
f6cb90e5dc feat: add logo to login 2024-10-09 07:28:49 +02:00
Fredrik Burmester
b878e93dec Merge pull request #163 from Alexk2309/master
Removed resumable items from next up
2024-10-08 19:55:28 +02:00
Fredrik Burmester
66cd36a899 feat: native selectable text for titles 2024-10-08 19:53:48 +02:00
Fredrik Burmester
91b926e6c2 fix: larger tap area 2024-10-08 19:53:38 +02:00
Fredrik Burmester
d4cc7499c0 Merge pull request #165 from fredrikburmester/refactor/player
Refactor: Update the player logic (video, music, live-tv)
2024-10-08 18:54:43 +02:00
Alex Kim
6012f8c8d2 Removed resumable items from next up 2024-10-09 02:40:11 +11:00
24 changed files with 299 additions and 133 deletions

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.17.0",
"version": "0.18.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -33,7 +33,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 43,
"versionCode": 46,
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive_icon.png"
},

View File

@@ -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: () => (
<View className="">
<Ionicons
name="file-tray-full-outline"
size={22}
color="white"
onPress={() => {
router.push("/logs");
}}
/>
</View>
),
}}
/>
{Object.entries(nestedTabPageScreenOptions).map(([name, options]) => (

View File

@@ -264,6 +264,7 @@ export default function index() {
fields: ["MediaSourceCount"],
limit: 20,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
enableResumable: false,
})
).data.Items || [],
type: "ScrollingCollectionList",

View File

@@ -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,28 +123,6 @@ 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 className="text-xs">{log.message}</Text>
</View>
))}
{logs?.length === 0 && (
<Text className="opacity-50">No logs available</Text>
)}
</View>
</View>
</View>
</ScrollView>
);

View File

@@ -32,7 +32,6 @@ import {
tagsFilterAtom,
yearFilterAtom,
} from "@/utils/atoms/filters";
import { orientationAtom } from "@/utils/atoms/orientation";
import {
BaseItemDto,
BaseItemDtoQueryResult,
@@ -44,6 +43,7 @@ import {
} from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useOrientation } from "@/hooks/useOrientation";
const MemoizedTouchableItemRouter = React.memo(TouchableItemRouter);
@@ -60,12 +60,13 @@ const Page = () => {
const [selectedTags, setSelectedTags] = useAtom(tagsFilterAtom);
const [sortBy, _setSortBy] = useAtom(sortByAtom);
const [sortOrder, _setSortOrder] = useAtom(sortOrderAtom);
const [orientation] = useAtom(orientationAtom);
const [sortByPreference, setSortByPreference] = useAtom(sortByPreferenceAtom);
const [sortOrderPreference, setOderByPreference] = useAtom(
sortOrderPreferenceAtom
);
const { orientation } = useOrientation();
useEffect(() => {
const sop = getSortOrderPreference(libraryId, sortOrderPreference);
if (sop) {
@@ -106,11 +107,12 @@ const Page = () => {
[libraryId, sortOrderPreference]
);
const getNumberOfColumns = useCallback(() => {
if (orientation === ScreenOrientation.Orientation.PORTRAIT_UP) return 3;
if (screenWidth < 600) return 5;
if (screenWidth < 960) return 6;
if (screenWidth < 1280) return 7;
const nrOfCols = useMemo(() => {
if (screenWidth < 300) return 2;
if (screenWidth < 500) return 3;
if (screenWidth < 800) return 5;
if (screenWidth < 1000) return 6;
if (screenWidth < 1500) return 7;
return 6;
}, [screenWidth, orientation]);
@@ -219,7 +221,7 @@ const Page = () => {
const renderItem = useCallback(
({ item, index }: { item: BaseItemDto; index: number }) => (
<MemoizedTouchableItemRouter
<TouchableItemRouter
key={item.Id}
style={{
width: "100%",
@@ -230,10 +232,10 @@ const Page = () => {
<View
style={{
alignSelf:
orientation === ScreenOrientation.Orientation.PORTRAIT_UP
? index % 3 === 0
orientation === ScreenOrientation.OrientationLock.PORTRAIT_UP
? index % nrOfCols === 0
? "flex-end"
: (index + 1) % 3 === 0
: (index + 1) % nrOfCols === 0
? "flex-start"
: "center"
: "center",
@@ -244,7 +246,7 @@ const Page = () => {
<ItemPoster item={item} />
<ItemCardText item={item} />
</View>
</MemoizedTouchableItemRouter>
</TouchableItemRouter>
),
[orientation]
);
@@ -429,6 +431,7 @@ const Page = () => {
return (
<FlashList
key={orientation}
ListEmptyComponent={
<View className="flex flex-col items-center justify-center h-full">
<Text className="font-bold text-xl text-neutral-500">No results</Text>
@@ -437,10 +440,10 @@ const Page = () => {
contentInsetAdjustmentBehavior="automatic"
data={flatData}
renderItem={renderItem}
extraData={orientation}
extraData={[orientation, nrOfCols]}
keyExtractor={keyExtractor}
estimatedItemSize={244}
numColumns={getNumberOfColumns()}
numColumns={nrOfCols}
onEndReached={() => {
if (hasNextPage) {
fetchNextPage();

View File

@@ -22,11 +22,9 @@ import { Dimensions, Pressable, StatusBar, View } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import Video, {
OnProgressData,
VideoRef,
SelectedTrack,
SelectedTrackType,
VideoRef,
} from "react-native-video";
import { WithDefault } from "react-native/Libraries/Types/CodegenTypes";
export default function page() {
const { playSettings, playUrl, playSessionId } = usePlaySettings();
@@ -216,18 +214,28 @@ export default function page() {
return (
<View
style={{
flex: 1,
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
width: screenDimensions.width,
height: screenDimensions.height,
position: "relative",
}}
className="flex flex-col items-center justify-center"
>
<StatusBar hidden />
<Pressable
onPress={() => {
setShowControls(!showControls);
}}
className="absolute z-0 h-full w-full"
style={{
position: "absolute",
top: 0,
left: 0,
width: screenDimensions.width,
height: screenDimensions.height,
zIndex: 0,
}}
>
<Video
ref={videoRef}

View File

@@ -345,6 +345,13 @@ function Layout() {
animation: "fade",
}}
/>
<Stack.Screen
name="logs"
options={{
presentation: "modal",
title: "Logs",
}}
/>
<Stack.Screen
name="(auth)/play-offline-video"
options={{

View File

@@ -2,10 +2,12 @@ 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 { useLocalSearchParams } from "expo-router";
import { Image } from "expo-image";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useAtom } from "jotai";
import React, { useEffect, useState } from "react";
import {
@@ -26,6 +28,7 @@ const Login: React.FC = () => {
const { setServer, login, removeServer, initiateQuickConnect } =
useJellyfin();
const [api] = useAtom(apiAtom);
const router = useRouter();
const params = useLocalSearchParams();
const {
@@ -71,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) {
@@ -104,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.error(`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);
}
}
@@ -195,16 +243,28 @@ const Login: React.FC = () => {
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1, height: "100%" }}
>
<View className="flex flex-col justify-between px-4 h-full gap-y-2">
<View></View>
<View>
<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">
{serverName || "Streamyfin"}
</Text>
<Text className="text-neutral-500 mb-2">
Server: {api.basePath}
</Text>
<View className="bg-neutral-900 rounded-xl p-4 mb-2 flex flex-row items-center justify-between">
<Text className="">URL</Text>
<Text numberOfLines={1} className="shrink">
{api.basePath}
</Text>
</View>
<Button
color="black"
onPress={() => {
@@ -261,11 +321,11 @@ const Login: React.FC = () => {
<Text className="text-red-600 mb-2">{error}</Text>
</View>
<View className="mt-auto mb-2">
<View className="absolute bottom-0 left-0 w-full px-4 mb-2">
<Button
color="black"
onPress={handleQuickConnect}
className="mb-2"
className="w-full mb-2"
>
Use Quick Connect
</Button>
@@ -285,9 +345,17 @@ const Login: React.FC = () => {
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View className="flex flex-col px-4 justify-between h-full">
<View></View>
<View className="flex flex-col gap-y-2">
<View className="flex flex-col h-full relative items-center justify-center w-full">
<View className="flex flex-col gap-y-2 px-4 w-full -mt-36">
<Image
style={{
width: 100,
height: 100,
marginLeft: -23,
marginBottom: -20,
}}
source={require("@/assets/images/StreamyFinFinal.png")}
/>
<Text className="text-3xl font-bold">Streamyfin</Text>
<Text className="text-neutral-500">
Connect to your Jellyfin server
@@ -303,14 +371,16 @@ const Login: React.FC = () => {
maxLength={500}
/>
</View>
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => await handleConnect(serverURL)}
className="mb-2"
>
Connect
</Button>
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
<Button
loading={loadingServerCheck}
disabled={loadingServerCheck}
onPress={async () => await handleConnect(serverURL)}
className="w-full grow"
>
Connect
</Button>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>

58
app/logs.tsx Normal file
View File

@@ -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 (
<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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
bun.lockb

Binary file not shown.

View File

@@ -26,7 +26,7 @@ import { useFocusEffect, useNavigation } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation";
import { useAtom } from "jotai";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { View } from "react-native";
import { Alert, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Chromecast } from "./Chromecast";
import { ItemHeader } from "./ItemHeader";
@@ -59,6 +59,11 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
audioIndex,
subtitleIndex,
});
if (!mediaSource) {
Alert.alert("Error", "No media source found for this item.");
navigation.goBack();
}
}, [item, settings])
);

View File

@@ -21,10 +21,10 @@ export const ListItem: React.FC<PropsWithChildren<Props>> = ({
className="flex flex-row items-center justify-between bg-neutral-900 p-4"
{...props}
>
<View className="flex flex-col">
<View className="flex flex-col overflow-visible">
<Text className="font-bold ">{title}</Text>
{subTitle && (
<Text className="text-xs" selectable>
<Text uiTextView selectable className="text-xs">
{subTitle}
</Text>
)}

View File

@@ -24,14 +24,14 @@ export const HeaderBackButton: React.FC<Props> = ({
if (background === "transparent" && Platform.OS !== "android")
return (
<BlurView
{...props}
intensity={100}
className="overflow-hidden rounded-full p-2"
<TouchableOpacity
onPress={() => router.back()}
{...touchableOpacityProps}
>
<TouchableOpacity
onPress={() => router.back()}
{...touchableOpacityProps}
<BlurView
{...props}
intensity={100}
className="overflow-hidden rounded-full p-2"
>
<Ionicons
className="drop-shadow-2xl"
@@ -39,8 +39,8 @@ export const HeaderBackButton: React.FC<Props> = ({
size={24}
color="white"
/>
</TouchableOpacity>
</BlurView>
</BlurView>
</TouchableOpacity>
);
return (

View File

@@ -10,9 +10,9 @@ export function Input(props: TextInputProps) {
className="p-4 border border-neutral-800 rounded-xl bg-neutral-900"
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}
placeholderTextColor={"#9CA3AF"}
clearButtonMode="while-editing"
{...otherProps}
/>
);
}

View File

@@ -1,11 +1,16 @@
import React from "react";
import { TextProps } from "react-native";
import { Text as DefaultText } from "react-native";
export function Text(props: TextProps) {
import { UITextView } from "react-native-uitextview";
export function Text(
props: TextProps & {
uiTextView?: boolean;
}
) {
const { style, ...otherProps } = props;
return (
<DefaultText
<UITextView
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}

View File

@@ -9,10 +9,10 @@ interface Props extends ViewProps {
export const MoviesTitleHeader: React.FC<Props> = ({ item, ...props }) => {
return (
<View {...props}>
<Text className=" font-bold text-2xl mb-1" selectable>
<Text uiTextView selectable className="font-bold text-2xl mb-1">
{item?.Name}
</Text>
<Text className=" opacity-50">{item?.ProductionYear}</Text>
<Text className="opacity-50">{item?.ProductionYear}</Text>
</View>
);
};

View File

@@ -12,7 +12,7 @@ export const EpisodeTitleHeader: React.FC<Props> = ({ item, ...props }) => {
return (
<View {...props}>
<Text className="font-bold text-2xl" selectable>
<Text uiTextView className="font-bold text-2xl" selectable>
{item?.Name}
</Text>
<View className="flex flex-row items-center mb-1">

View File

@@ -123,17 +123,6 @@ export const Controls: React.FC<Props> = ({
const wasPlayingRef = useRef(false);
const updateTimes = useCallback(
(currentProgress: number, maxValue: number) => {
const current = ticksToSeconds(currentProgress);
const remaining = ticksToSeconds(maxValue - currentProgress);
setCurrentTime(current);
setRemainingTime(remaining);
},
[]
);
const { showSkipButton, skipIntro } = useIntroSkipper(
item.Id,
currentTime,
@@ -180,6 +169,23 @@ export const Controls: React.FC<Props> = ({
router.replace("/play-video");
}, [nextItem, settings]);
const updateTimes = useCallback(
(currentProgress: number, maxValue: number) => {
const current = ticksToSeconds(currentProgress);
const remaining = ticksToSeconds(maxValue - currentProgress);
setCurrentTime(current);
setRemainingTime(remaining);
if (currentProgress === maxValue) {
setShowControls(true);
// Automatically play the next item if it exists
goToNextItem();
}
},
[goToNextItem]
);
useAnimatedReaction(
() => ({
progress: progress.value,

View File

@@ -22,13 +22,13 @@
}
},
"production": {
"channel": "0.17.0",
"channel": "0.18.0",
"android": {
"image": "latest"
}
},
"production-apk": {
"channel": "0.17.0",
"channel": "0.18.0",
"android": {
"buildType": "apk",
"image": "latest"

View File

@@ -82,6 +82,7 @@
"react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-tab-view": "^3.5.2",
"react-native-uitextview": "^1.4.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.2",
"react-native-video": "^6.6.4",

View File

@@ -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";
@@ -52,7 +53,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin(
() =>
new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.17.0" },
clientInfo: { name: "Streamyfin", version: "0.18.0" },
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
})
);
@@ -86,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return {
authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.17.0"`,
}, DeviceId="${deviceId}", Version="0.18.0"`,
};
}, [deviceId]);
@@ -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");

View File

@@ -9,7 +9,7 @@ import { Settings } from "../atoms/settings";
interface PlaySettings {
item: BaseItemDto;
bitrate: (typeof BITRATES)[0];
mediaSource: MediaSourceInfo | undefined;
mediaSource?: MediaSourceInfo | null;
audioIndex?: number | null;
subtitleIndex?: number | null;
}
@@ -29,9 +29,8 @@ export function getDefaultPlaySettings(
}
// 1. Get first media source
const mediaSource = item.MediaSources?.[0];
if (!mediaSource) throw new Error("No media source found");
const mediaSource = item.MediaSources?.[0];
// 2. Get default or preferred audio
const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex;

View File

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