diff --git a/app/login.tsx b/app/login.tsx index 61cf7ca6..eadd8969 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -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 () => { diff --git a/components/Button.tsx b/components/Button.tsx index 305312d4..1498a975 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -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 { +export interface ButtonProps + extends React.ComponentProps { onPress?: () => void; className?: string; textClassName?: string; diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 04d3a12f..d23ddc6d 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -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}`; }; diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 5c5d67a0..bedfeb31 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -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 = ({ } = 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 = ({ ? 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 = ({ 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 = ({ 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 = ({ right: 0, left: 0, bottom: 0, - opacity: showControls ? 1 : 0, }, ]} - pointerEvents={showControls ? "box-none" : "none"} className={`flex flex-col p-4`} > = ({ style={{ flexDirection: "column", alignSelf: "flex-end", // Shrink height based on content + opacity: showControls ? 1 : 0, }} + pointerEvents={showControls ? "box-none" : "none"} > {item?.Name} {item?.Type === "Episode" && ( @@ -745,13 +739,7 @@ export const Controls: React.FC = ({ {item?.Album} )} - + = ({ onPress={skipCredit} buttonText="Skip Credits" /> + void; + onPress?: () => void; + show: boolean; +} + +const NextEpisodeCountDownButton: React.FC = ({ + 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 ( + + + + Next Episode + + + ); +}; + +export default NextEpisodeCountDownButton; diff --git a/components/video-player/controls/SkipButton.tsx b/components/video-player/controls/SkipButton.tsx index aa5df7ec..15bd5fa6 100644 --- a/components/video-player/controls/SkipButton.tsx +++ b/components/video-player/controls/SkipButton.tsx @@ -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 = ({ onPress, showButton, buttonText, + ...props }) => { return ( - - - {buttonText} + + + {buttonText} ); }; -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;