Merged main into branch

This commit is contained in:
Alex Kim
2024-12-12 04:25:37 +11:00
6 changed files with 136 additions and 78 deletions

View File

@@ -103,37 +103,19 @@ const Login: React.FC = () => {
* - Logs errors and timeout information to the console.
*/
async function checkUrl(url: string) {
url = url.endsWith("/") ? url.slice(0, -1) : url;
setLoadingServerCheck(true);
const protocols = ["https://", "http://"];
const timeout = 2000; // 2 seconds timeout for long 404 responses
try {
for (const protocol of protocols) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(`${url}/System/Info/Public`, {
mode: "cors",
});
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.error(`Request to ${protocol}${url} timed out`);
} else {
console.error(`Error checking ${protocol}${url}:`, error);
}
}
if (response.ok) {
const data = (await response.json()) as PublicSystemInfo;
setServerName(data.ServerName || "");
return url;
}
return undefined;
} finally {
setLoadingServerCheck(false);
@@ -159,9 +141,7 @@ const Login: React.FC = () => {
const handleConnect = async (url: string) => {
url = url.trim();
const result = await checkUrl(
url.startsWith("http") ? new URL(url).host : url
);
const result = await checkUrl(url);
if (result === undefined) {
Alert.alert(
@@ -171,7 +151,7 @@ const Login: React.FC = () => {
return;
}
setServer({ address: result });
setServer({ address: url });
};
const handleQuickConnect = async () => {

View File

@@ -3,7 +3,8 @@ import React, { PropsWithChildren, ReactNode, useMemo } from "react";
import { Text, TouchableOpacity, View } from "react-native";
import { Loader } from "./Loader";
interface ButtonProps extends React.ComponentProps<typeof TouchableOpacity> {
export interface ButtonProps
extends React.ComponentProps<typeof TouchableOpacity> {
onPress?: () => void;
className?: string;
textClassName?: string;

View File

@@ -45,6 +45,10 @@ export const itemRouter = (item: BaseItemDto, from: string) => {
return `/(auth)/(tabs)/(libraries)/${item.Id}`;
}
if (item.Type === "Playlist") {
return `/(auth)/(tabs)/(libraries)/${item.Id}`;
}
return `/(auth)/(tabs)/${from}/items/page?id=${item.Id}`;
};

View File

@@ -8,11 +8,13 @@ import {
TrackInfo,
VlcPlayerViewRef,
} from "@/modules/vlc-player/src/VlcPlayer.types";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import {
getDefaultPlaySettings,
previousIndexes,
} from "@/utils/jellyfin/getDefaultPlaySettings";
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
import { writeToLog } from "@/utils/log";
import {
formatTimeString,
@@ -26,8 +28,11 @@ import {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client";
import * as Haptics from "expo-haptics";
import { Image } from "expo-image";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useAtom } from "jotai";
import { debounce } from "lodash";
import { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions, Pressable, TouchableOpacity, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
@@ -39,19 +44,15 @@ import {
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { VideoRef } from "react-native-video";
import AudioSlider from "./AudioSlider";
import BrightnessSlider from "./BrightnessSlider";
import { ControlProvider } from "./contexts/ControlContext";
import { VideoProvider } from "./contexts/VideoContext";
import * as Haptics from "expo-haptics";
import DropdownViewDirect from "./dropdown/DropdownViewDirect";
import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
import BrightnessSlider from "./BrightnessSlider";
import SkipButton from "./SkipButton";
import { debounce } from "lodash";
import { EpisodeList } from "./EpisodeList";
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
import { useAtom } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
import AudioSlider from "./AudioSlider";
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
import SkipButton from "./SkipButton";
interface Props {
item: BaseItemDto;
@@ -123,7 +124,7 @@ export const Controls: React.FC<Props> = ({
} = useTrickplay(item, !offline && enableTrickplay);
const [currentTime, setCurrentTime] = useState(0);
const [remainingTime, setRemainingTime] = useState(0);
const [remainingTime, setRemainingTime] = useState(Infinity);
const min = useSharedValue(0);
const max = useSharedValue(item.RunTimeTicks || 0);
@@ -236,15 +237,10 @@ export const Controls: React.FC<Props> = ({
? maxValue - currentProgress
: ticksToSeconds(maxValue - currentProgress);
console.log("remaining: ", remaining);
setCurrentTime(current);
setRemainingTime(remaining);
// Currently doesm't work in VLC because of some corrupted timestamps, will need to find a workaround.
if (currentProgress === maxValue) {
setShowControls(true);
// Automatically play the next item if it exists
goToNextItem();
}
},
[goToNextItem, isVlc]
);
@@ -256,7 +252,6 @@ export const Controls: React.FC<Props> = ({
isSeeking: isSeeking.value,
}),
(result) => {
// console.log("Progress changed", result);
if (result.isSeeking === false) {
runOnJS(updateTimes)(result.progress, result.max);
}
@@ -317,7 +312,6 @@ export const Controls: React.FC<Props> = ({
const handleSliderChange = useCallback(
debounce((value: number) => {
const progressInTicks = isVlc ? msToTicks(value) : value;
console.log("Progress in ticks", progressInTicks);
calculateTrickplayUrl(progressInTicks);
const progressInSeconds = Math.floor(ticksToSeconds(progressInTicks));
const hours = Math.floor(progressInSeconds / 3600);
@@ -713,10 +707,8 @@ export const Controls: React.FC<Props> = ({
right: 0,
left: 0,
bottom: 0,
opacity: showControls ? 1 : 0,
},
]}
pointerEvents={showControls ? "box-none" : "none"}
className={`flex flex-col p-4`}
>
<View
@@ -730,7 +722,9 @@ export const Controls: React.FC<Props> = ({
style={{
flexDirection: "column",
alignSelf: "flex-end", // Shrink height based on content
opacity: showControls ? 1 : 0,
}}
pointerEvents={showControls ? "box-none" : "none"}
>
<Text className="font-bold">{item?.Name}</Text>
{item?.Type === "Episode" && (
@@ -745,13 +739,7 @@ export const Controls: React.FC<Props> = ({
<Text className="text-xs opacity-50">{item?.Album}</Text>
)}
</View>
<View
style={{
flexDirection: "column",
alignSelf: "flex-end",
marginRight: insets.right,
}}
>
<View className="flex flex-row space-x-2">
<SkipButton
showButton={showSkipButton}
onPress={skipIntro}
@@ -762,10 +750,25 @@ export const Controls: React.FC<Props> = ({
onPress={skipCredit}
buttonText="Skip Credits"
/>
<NextEpisodeCountDownButton
show={
!nextItem
? false
: isVlc
? remainingTime < 10000
: remainingTime < 10
}
onFinish={goToNextItem}
onPress={goToNextItem}
/>
</View>
</View>
<View
className={`flex flex-col-reverse py-4 pb-1 px-4 rounded-lg items-center bg-neutral-800`}
style={{
opacity: showControls ? 1 : 0,
}}
pointerEvents={showControls ? "box-none" : "none"}
>
<View className={`flex flex-col w-full shrink`}>
<Slider

View File

@@ -0,0 +1,81 @@
import React, { useEffect } from "react";
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
import { Text } from "@/components/common/Text";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
runOnJS,
} from "react-native-reanimated";
import { Colors } from "@/constants/Colors";
interface NextEpisodeCountDownButtonProps extends TouchableOpacityProps {
onFinish?: () => void;
onPress?: () => void;
show: boolean;
}
const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
onFinish,
onPress,
show,
...props
}) => {
const progress = useSharedValue(0);
useEffect(() => {
if (show) {
progress.value = 0;
progress.value = withTiming(
1,
{
duration: 10000, // 10 seconds
easing: Easing.linear,
},
(finished) => {
if (finished && onFinish) {
console.log("finish");
runOnJS(onFinish)();
}
}
);
}
}, [show, onFinish]);
const animatedStyle = useAnimatedStyle(() => {
return {
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: `${progress.value * 100}%`,
backgroundColor: Colors.primary,
};
});
const handlePress = () => {
if (onPress) {
onPress();
}
};
if (!show) {
return null;
}
return (
<TouchableOpacity
className="w-32 overflow-hidden rounded-md bg-black/60 border border-neutral-900"
{...props}
onPress={handlePress}
>
<Animated.View style={animatedStyle} />
<View className="px-3 py-3">
<Text className="text-center font-bold">Next Episode</Text>
</View>
</TouchableOpacity>
);
};
export default NextEpisodeCountDownButton;

View File

@@ -1,7 +1,7 @@
import React from "react";
import { View, TouchableOpacity, Text, StyleSheet } from "react-native";
import { View, TouchableOpacity, Text, ViewProps } from "react-native";
interface SkipButtonProps {
interface SkipButtonProps extends ViewProps {
onPress: () => void;
showButton: boolean;
buttonText: string;
@@ -11,29 +11,18 @@ const SkipButton: React.FC<SkipButtonProps> = ({
onPress,
showButton,
buttonText,
...props
}) => {
return (
<View style={{ display: showButton ? "flex" : "none" }}>
<TouchableOpacity onPress={onPress} style={styles.button}>
<Text style={styles.text}>{buttonText}</Text>
<View className={showButton ? "flex" : "hidden"} {...props}>
<TouchableOpacity
onPress={onPress}
className="bg-black/60 rounded-md px-3 py-3 border border-neutral-900"
>
<Text className="text-white font-bold">{buttonText}</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
button: {
backgroundColor: "rgba(0, 0, 0, 0.75)",
borderRadius: 5,
paddingHorizontal: 10,
paddingVertical: 15,
borderWidth: 2,
borderColor: "#5A5454",
},
text: {
color: "white",
fontWeight: "bold",
},
});
export default SkipButton;