diff --git a/app/(auth)/offline-player.tsx b/app/(auth)/offline-player.tsx new file mode 100644 index 00000000..5caa17b5 --- /dev/null +++ b/app/(auth)/offline-player.tsx @@ -0,0 +1,167 @@ +import { Controls } from "@/components/video-player/Controls"; +import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar"; +import { useOrientation } from "@/hooks/useOrientation"; +import { useOrientationSettings } from "@/hooks/useOrientationSettings"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { + PlaybackType, + usePlaySettings, +} from "@/providers/PlaySettingsProvider"; +import { secondsToTicks } from "@/utils/secondsToTicks"; +import { Api } from "@jellyfin/sdk"; +import * as Haptics from "expo-haptics"; +import { useFocusEffect } from "expo-router"; +import { useAtomValue } from "jotai"; +import React, { useCallback, useMemo, useRef, useState } from "react"; +import { Pressable, StatusBar, useWindowDimensions, View } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; +import Video, { OnProgressData, VideoRef } from "react-native-video"; + +export default function page() { + const { playSettings, playUrl } = usePlaySettings(); + + const api = useAtomValue(apiAtom); + const videoRef = useRef(null); + const videoSource = useVideoSource(playSettings, api, playUrl); + const firstTime = useRef(true); + + const dimensions = useWindowDimensions(); + useOrientation(); + useOrientationSettings(); + useAndroidNavigationBar(); + + const [showControls, setShowControls] = useState(true); + const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [isBuffering, setIsBuffering] = useState(true); + + const progress = useSharedValue(0); + const isSeeking = useSharedValue(false); + const cacheProgress = useSharedValue(0); + + const togglePlay = useCallback(async () => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (isPlaying) { + videoRef.current?.pause(); + } else { + videoRef.current?.resume(); + } + }, [isPlaying]); + + const play = useCallback(() => { + setIsPlaying(true); + videoRef.current?.resume(); + }, [videoRef]); + + const stop = useCallback(() => { + setIsPlaying(false); + videoRef.current?.pause(); + }, [videoRef]); + + useFocusEffect( + useCallback(() => { + play(); + + return () => { + stop(); + }; + }, [play, stop]) + ); + + const onProgress = useCallback(async (data: OnProgressData) => { + if (isSeeking.value === true) return; + progress.value = secondsToTicks(data.currentTime); + cacheProgress.value = secondsToTicks(data.playableDuration); + setIsBuffering(data.playableDuration === 0); + }, []); + + if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item) + return null; + + return ( + + + ); +} + +export function useVideoSource( + playSettings: PlaybackType | null, + api: Api | null, + playUrl?: string | null +) { + const videoSource = useMemo(() => { + if (!playSettings || !api || !playUrl) { + return null; + } + + const startPosition = 0; + + return { + uri: playUrl, + isNetwork: false, + startPosition, + metadata: { + artist: playSettings.item?.AlbumArtist ?? undefined, + title: playSettings.item?.Name || "Unknown", + description: playSettings.item?.Overview ?? undefined, + subtitle: playSettings.item?.Album ?? undefined, + }, + }; + }, [playSettings, api]); + + return videoSource; +} diff --git a/app/(auth)/play-offline-video.tsx b/app/(auth)/offline-vlc-player.tsx similarity index 100% rename from app/(auth)/play-offline-video.tsx rename to app/(auth)/offline-vlc-player.tsx diff --git a/app/_layout.tsx b/app/_layout.tsx index f7840be8..4b2f74a8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -341,7 +341,16 @@ function Layout() { }} /> + = ({ ...props }) => { return; } - router.push("/vlc-player"); + if (Platform.OS === "ios") router.push("/vlc-player"); + else router.push("/play-video"); return; } diff --git a/hooks/useDownloadedFileOpener.ts b/hooks/useDownloadedFileOpener.ts index 8a1e4d1c..67244d2c 100644 --- a/hooks/useDownloadedFileOpener.ts +++ b/hooks/useDownloadedFileOpener.ts @@ -6,6 +6,7 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import * as FileSystem from "expo-file-system"; import { useRouter } from "expo-router"; import { useCallback } from "react"; +import { Platform } from "react-native"; export const useFileOpener = () => { const router = useRouter(); @@ -38,7 +39,8 @@ export const useFileOpener = () => { }); setPlayUrl(url); - router.push("/play-offline-video"); + if (Platform.OS === "ios") router.push("/offline-vlc-player"); + else router.push("/offline-player"); } catch (error) { writeToLog("ERROR", "Error opening file", error); console.error("Error opening file:", error);