diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index b7e9b627..f46bf058 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -273,47 +273,47 @@ export default function index() { mediaListCollections, ]); - // if (isConnected === false) { - // return ( - // - // No Internet - // - // No worries, you can still watch{"\n"}downloaded content. - // - // - // - // - // - // - // ); - // } + if (isConnected === false) { + return ( + + No Internet + + No worries, you can still watch{"\n"}downloaded content. + + + + + + + ); + } const insets = useSafeAreaInsets(); diff --git a/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx b/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx index 9eca5928..c496ea88 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/items/page.tsx @@ -1,4 +1,3 @@ -import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar"; import { ItemContent } from "@/components/ItemContent"; import { Stack, useLocalSearchParams } from "expo-router"; import React from "react"; diff --git a/app/_layout.tsx b/app/_layout.tsx index 1f4d5131..cac8f2ea 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,4 +1,4 @@ -import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar"; +import { FullScreenVideoPlayer } from "@/components/FullScreenVideoPlayer"; import { JellyfinProvider } from "@/providers/JellyfinProvider"; import { JobQueueProvider } from "@/providers/JobQueueProvider"; import { PlaybackProvider } from "@/providers/PlaybackProvider"; @@ -126,7 +126,7 @@ function Layout() { /> - + diff --git a/components/CurrentlyPlayingBar.tsx b/components/FullScreenVideoPlayer.tsx similarity index 68% rename from components/CurrentlyPlayingBar.tsx rename to components/FullScreenVideoPlayer.tsx index d47e19cc..946e9311 100644 --- a/components/CurrentlyPlayingBar.tsx +++ b/components/FullScreenVideoPlayer.tsx @@ -18,6 +18,8 @@ import { useAtom } from "jotai"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, + AppState, + AppStateStatus, Dimensions, Pressable, TouchableOpacity, @@ -26,6 +28,7 @@ import { import { Slider } from "react-native-awesome-slider"; import "react-native-gesture-handler"; import Animated, { + runOnJS, useAnimatedStyle, useSharedValue, withTiming, @@ -35,6 +38,7 @@ import Video, { OnProgressData } from "react-native-video"; import { Text } from "./common/Text"; import { itemRouter } from "./common/TouchableItemRouter"; import { Loader } from "./Loader"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; async function setOrientation(orientation: ScreenOrientation.OrientationLock) { await ScreenOrientation.lockAsync(orientation); @@ -44,7 +48,7 @@ async function resetOrientation() { await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT); } -export const CurrentlyPlayingBar: React.FC = () => { +export const FullScreenVideoPlayer: React.FC = () => { const { currentlyPlaying, pauseVideo, @@ -68,7 +72,8 @@ export const CurrentlyPlayingBar: React.FC = () => { const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(currentlyPlaying); const { previousItem, nextItem } = useAdjacentEpisodes({ currentlyPlaying }); - const { isVisible, showControls, hideControls } = useControlsVisibility(3000); + const { showControls, hideControls, opacity } = useControlsVisibility(3000); + const [isInteractive, setIsInteractive] = useState(true); const [ignoreSafeArea, setIgnoreSafeArea] = useState(false); const from = useMemo(() => segments[2], [segments]); @@ -82,14 +87,6 @@ export const CurrentlyPlayingBar: React.FC = () => { const { width: screenWidth, height: screenHeight } = Dimensions.get("window"); - const toggleIgnoreSafeArea = useCallback(() => { - setIgnoreSafeArea((prev) => !prev); - }, []); - - const handleToggleControlsPress = useCallback(() => { - isVisible ? hideControls() : showControls(); - }, [isVisible, hideControls, showControls]); - const poster = useMemo(() => { if (!currentlyPlaying?.item || !api) return ""; return currentlyPlaying.item.Type === "Audio" @@ -122,6 +119,26 @@ export const CurrentlyPlayingBar: React.FC = () => { }; }, [currentlyPlaying, api, poster]); + useEffect(() => { + const handleAppStateChange = (nextAppState: AppStateStatus) => { + if (nextAppState === "active") { + setIsInteractive(true); + showControls(); + } else { + setIsInteractive(false); + } + }; + + const subscription = AppState.addEventListener( + "change", + handleAppStateChange + ); + + return () => { + subscription.remove(); + }; + }, [showControls]); + useEffect(() => { max.value = currentlyPlaying?.item.RunTimeTicks || 0; }, [currentlyPlaying?.item.RunTimeTicks]); @@ -150,12 +167,15 @@ export const CurrentlyPlayingBar: React.FC = () => { const animatedStyles = { controls: useAnimatedStyle(() => ({ - opacity: withTiming(isVisible ? 1 : 0, { duration: 300 }), + opacity: withTiming(opacity.value, { duration: 300 }), })), videoContainer: useAnimatedStyle(() => ({ - opacity: withTiming(isVisible || localIsBuffering.value ? 0.5 : 1, { - duration: 300, - }), + opacity: withTiming( + opacity.value === 1 || localIsBuffering.value ? 0.5 : 1, + { + duration: 300, + } + ), })), loader: useAnimatedStyle(() => ({ opacity: withTiming(localIsBuffering.value ? 1 : 0, { duration: 300 }), @@ -200,7 +220,9 @@ export const CurrentlyPlayingBar: React.FC = () => { progress.value > showButtonAt && progress.value < hideButtonAt; return { opacity: withTiming( - localIsBuffering.value === false && isVisible && showButton ? 1 : 0, + localIsBuffering.value === false && opacity.value === 1 && showButton + ? 1 + : 0, { duration: 300, } @@ -208,6 +230,18 @@ export const CurrentlyPlayingBar: React.FC = () => { }; }); + const toggleIgnoreSafeArea = useCallback(() => { + setIgnoreSafeArea((prev) => !prev); + }, []); + + const handleToggleControlsPress = useCallback(() => { + if (opacity.value === 1) { + hideControls(); + } else { + showControls(); + } + }, [opacity.value, hideControls, showControls]); + const skipIntro = useCallback(async () => { if (!introTimestamps || !videoRef.current) return; try { @@ -298,7 +332,67 @@ export const CurrentlyPlayingBar: React.FC = () => { [progress, calculateTrickplayUrl, showControls] ); - if (!api || !currentlyPlaying) return null; + const handleGoToPreviousItem = useCallback(() => { + if (!previousItem || !from) return; + const url = itemRouter(previousItem, from); + stopPlayback(); + // @ts-ignore + router.push(url); + }, [previousItem, from, stopPlayback, router]); + + const handleGoToNextItem = useCallback(() => { + if (!nextItem || !from) return; + const url = itemRouter(nextItem, from); + stopPlayback(); + // @ts-ignore + router.push(url); + }, [nextItem, from, stopPlayback, router]); + + const videoTap = Gesture.Tap().onBegin(() => { + runOnJS(handleToggleControlsPress)(); + }); + + const toggleIgnoreSafeAreaGesture = Gesture.Tap() + .enabled(opacity.value !== 0) + .onStart(() => { + runOnJS(toggleIgnoreSafeArea)(); + }); + + const playPauseGesture = Gesture.Tap() + .enabled(opacity.value !== 0) + .onStart(() => { + runOnJS(handlePlayPause)(); + }); + + const goToPreviouItemGesture = Gesture.Tap() + .enabled(opacity.value !== 0) + .onStart(() => { + runOnJS(handleGoToPreviousItem)(); + }); + + const goToNextItemGesture = Gesture.Tap() + .enabled(opacity.value !== 0) + .onStart(() => { + runOnJS(handleGoToNextItem)(); + }); + + const skipBackwardGesture = Gesture.Tap() + .enabled(opacity.value !== 0) + .onStart(() => { + runOnJS(handleSkipBackward)(); + }); + + const skipForwardGesture = Gesture.Tap() + .enabled(opacity.value !== 0) + .onStart(() => { + runOnJS(handleSkipForward)(); + }); + + const skipIntroGesture = Gesture.Tap() + .enabled(opacity.value !== 0) + .onStart(() => { + runOnJS(skipIntro)(); + }); if (!api || !currentlyPlaying) return null; @@ -310,72 +404,74 @@ export const CurrentlyPlayingBar: React.FC = () => { backgroundColor: "black", }} > - - + - {videoSource && ( - - + + {videoSource && ( + + + { style={[ { position: "absolute", - bottom: insets.bottom + 8 * 7, + bottom: insets.bottom + 8 * 8, right: insets.right + 32, + zIndex: 10, }, animatedIntroSkipperStyle, ]} > - { - skipIntro(); - }} - className="flex flex-col items-center justify-center px-2 py-1.5 bg-purple-600 rounded-full" - > - Skip intro + + + Skip intro + @@ -433,21 +526,17 @@ export const CurrentlyPlayingBar: React.FC = () => { ]} > + + + + + + { - toggleIgnoreSafeArea(); - }} - className="aspect-square rounded flex flex-col items-center justify-center p-2" - > - - - { stopPlayback(); }} @@ -488,65 +577,56 @@ export const CurrentlyPlayingBar: React.FC = () => { )} - + { - if (!previousItem || !from) return; - const url = itemRouter(previousItem, from); - stopPlayback(); - // @ts-ignore - router.push(url); - }} > - + + + + + + + + + + + + + + + + + + - - - - - - - - - { - if (!nextItem || !from) return; - const url = itemRouter(nextItem, from); - stopPlayback(); - // @ts-ignore - router.push(url); - }} > - + + + { height: tileHeight, marginLeft: -tileWidth / 4, marginTop: -tileHeight / 4 - 60, + marginBottom: 10, }} className=" bg-neutral-800 overflow-hidden" > @@ -597,17 +678,17 @@ export const CurrentlyPlayingBar: React.FC = () => { ); }} - sliderHeight={8} + sliderHeight={10} thumbWidth={0} progress={progress} minimumValue={min} maximumValue={max} /> - - + + {runtimeTicksToSeconds(progress.value)} - + -{runtimeTicksToSeconds(max.value - progress.value)} diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index 8d8e577d..ca4831cf 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -97,22 +97,6 @@ export const SettingToggles: React.FC = ({ ...props }) => { /> - - - Start videos in fullscreen - - Clicking a video will start it in fullscreen mode, instead of - inline. - - - - updateSettings({ openFullScreenVideoPlayerByDefault: value }) - } - /> - - Use external player (VLC) diff --git a/hooks/useControlsVisibility.ts b/hooks/useControlsVisibility.ts index 6d829f06..964c296a 100644 --- a/hooks/useControlsVisibility.ts +++ b/hooks/useControlsVisibility.ts @@ -1,23 +1,29 @@ -import { useRef, useCallback, useState, useEffect } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { + runOnJS, + useAnimatedReaction, + useSharedValue, +} from "react-native-reanimated"; export const useControlsVisibility = (timeout: number = 3000) => { - const [isVisible, setIsVisible] = useState(true); + const opacity = useSharedValue(1); + const hideControlsTimerRef = useRef | null>( null ); const showControls = useCallback(() => { - setIsVisible(true); + opacity.value = 1; if (hideControlsTimerRef.current) { clearTimeout(hideControlsTimerRef.current); } hideControlsTimerRef.current = setTimeout(() => { - setIsVisible(false); + opacity.value = 0; }, timeout); }, [timeout]); const hideControls = useCallback(() => { - setIsVisible(false); + opacity.value = 0; if (hideControlsTimerRef.current) { clearTimeout(hideControlsTimerRef.current); } @@ -31,5 +37,5 @@ export const useControlsVisibility = (timeout: number = 3000) => { }; }, []); - return { isVisible, showControls, hideControls }; + return { opacity, showControls, hideControls }; }; diff --git a/providers/PlaybackProvider.tsx b/providers/PlaybackProvider.tsx index d98a79a3..22c54631 100644 --- a/providers/PlaybackProvider.tsx +++ b/providers/PlaybackProvider.tsx @@ -114,13 +114,8 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({ setCurrentlyPlaying(state); setIsPlaying(true); - if (settings?.openFullScreenVideoPlayerByDefault) { - setTimeout(() => { - presentFullscreenPlayer(); - }, 300); - } }, - [settings?.openFullScreenVideoPlayerByDefault] + [] ); const setCurrentlyPlayingState = useCallback( diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 88528cf7..2a09ca93 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -57,7 +57,6 @@ export type DefaultLanguageOption = { type Settings = { autoRotate?: boolean; forceLandscapeInVideoPlayer?: boolean; - openFullScreenVideoPlayerByDefault?: boolean; usePopularPlugin?: boolean; deviceProfile?: "Expo" | "Native" | "Old"; forceDirectPlay?: boolean; @@ -85,7 +84,6 @@ const loadSettings = async (): Promise => { const defaultValues: Settings = { autoRotate: true, forceLandscapeInVideoPlayer: false, - openFullScreenVideoPlayerByDefault: false, usePopularPlugin: false, deviceProfile: "Expo", forceDirectPlay: false,