mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-11 23:12:55 +01:00
Merge pull request #394 from Ryan0204/enhancement/autohidecontrol
enhancement: auto hide control after 5 seconds
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||||
@@ -6,9 +7,11 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { Dimensions, TouchableOpacity, View, ViewProps } from "react-native";
|
import { Dimensions, View, ViewProps } from "react-native";
|
||||||
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -18,11 +21,7 @@ import Carousel, {
|
|||||||
ICarouselInstance,
|
ICarouselInstance,
|
||||||
Pagination,
|
Pagination,
|
||||||
} from "react-native-reanimated-carousel";
|
} from "react-native-reanimated-carousel";
|
||||||
import { itemRouter, TouchableItemRouter } from "../common/TouchableItemRouter";
|
import { itemRouter } from "../common/TouchableItemRouter";
|
||||||
import { Loader } from "../Loader";
|
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
||||||
import { useRouter, useSegments } from "expo-router";
|
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,7 @@ import * as ScreenOrientation from "expo-screen-orientation";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
|
||||||
Pressable,
|
|
||||||
TouchableOpacity,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
import {
|
import {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
@@ -59,6 +54,8 @@ import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
|
|||||||
import { EpisodeList } from "./EpisodeList";
|
import { EpisodeList } from "./EpisodeList";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
import SkipButton from "./SkipButton";
|
import SkipButton from "./SkipButton";
|
||||||
|
import { useControlsTimeout } from "./useControlsTimeout";
|
||||||
|
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -89,6 +86,8 @@ interface Props {
|
|||||||
isVlc?: boolean;
|
isVlc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONTROLS_TIMEOUT = 4000;
|
||||||
|
|
||||||
export const Controls: React.FC<Props> = ({
|
export const Controls: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
seek,
|
seek,
|
||||||
@@ -121,6 +120,12 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
|
|
||||||
|
const [episodeView, setEpisodeView] = useState(false);
|
||||||
|
const [isSliding, setIsSliding] = useState(false);
|
||||||
|
|
||||||
|
// Used when user changes audio through audio button on device.
|
||||||
|
const [showAudioSlider, setShowAudioSlider] = useState(false);
|
||||||
|
|
||||||
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
const { height: screenHeight, width: screenWidth } = useWindowDimensions();
|
||||||
const { previousItem, nextItem } = useAdjacentItems({ item });
|
const { previousItem, nextItem } = useAdjacentItems({ item });
|
||||||
const {
|
const {
|
||||||
@@ -139,6 +144,23 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const wasPlayingRef = useRef(false);
|
const wasPlayingRef = useRef(false);
|
||||||
const lastProgressRef = useRef<number>(0);
|
const lastProgressRef = useRef<number>(0);
|
||||||
|
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
prefetchAllTrickplayImages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (item) {
|
||||||
|
progress.value = isVlc
|
||||||
|
? ticksToMs(item?.UserData?.PlaybackPositionTicks)
|
||||||
|
: item?.UserData?.PlaybackPositionTicks || 0;
|
||||||
|
max.value = isVlc
|
||||||
|
? ticksToMs(item.RunTimeTicks || 0)
|
||||||
|
: item.RunTimeTicks || 0;
|
||||||
|
}
|
||||||
|
}, [item, isVlc]);
|
||||||
|
|
||||||
const { bitrateValue, subtitleIndex, audioIndex } = useLocalSearchParams<{
|
const { bitrateValue, subtitleIndex, audioIndex } = useLocalSearchParams<{
|
||||||
bitrateValue: string;
|
bitrateValue: string;
|
||||||
audioIndex: string;
|
audioIndex: string;
|
||||||
@@ -161,8 +183,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
isVlc
|
isVlc
|
||||||
);
|
);
|
||||||
|
|
||||||
const lightHapticFeedback = useHaptic("light");
|
|
||||||
|
|
||||||
const goToPreviousItem = useCallback(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
if (!previousItem || !settings) return;
|
if (!previousItem || !settings) return;
|
||||||
|
|
||||||
@@ -266,20 +286,19 @@ export const Controls: React.FC<Props> = ({
|
|||||||
[updateTimes]
|
[updateTimes]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const hideControls = useCallback(() => {
|
||||||
if (item) {
|
setShowControls(false);
|
||||||
progress.value = isVlc
|
setShowAudioSlider(false);
|
||||||
? ticksToMs(item?.UserData?.PlaybackPositionTicks)
|
|
||||||
: item?.UserData?.PlaybackPositionTicks || 0;
|
|
||||||
max.value = isVlc
|
|
||||||
? ticksToMs(item.RunTimeTicks || 0)
|
|
||||||
: item.RunTimeTicks || 0;
|
|
||||||
}
|
|
||||||
}, [item, isVlc]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
prefetchAllTrickplayImages();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { handleControlsInteraction } = useControlsTimeout({
|
||||||
|
showControls,
|
||||||
|
isSliding,
|
||||||
|
episodeView,
|
||||||
|
onHideControls: hideControls,
|
||||||
|
timeout: CONTROLS_TIMEOUT,
|
||||||
|
});
|
||||||
|
|
||||||
const toggleControls = () => {
|
const toggleControls = () => {
|
||||||
if (showControls) {
|
if (showControls) {
|
||||||
setShowAudioSlider(false);
|
setShowAudioSlider(false);
|
||||||
@@ -300,16 +319,13 @@ export const Controls: React.FC<Props> = ({
|
|||||||
isSeeking.value = true;
|
isSeeking.value = true;
|
||||||
}, [showControls, isPlaying]);
|
}, [showControls, isPlaying]);
|
||||||
|
|
||||||
const [isSliding, setIsSliding] = useState(false);
|
|
||||||
const handleSliderComplete = useCallback(
|
const handleSliderComplete = useCallback(
|
||||||
async (value: number) => {
|
async (value: number) => {
|
||||||
isSeeking.value = false;
|
isSeeking.value = false;
|
||||||
progress.value = value;
|
progress.value = value;
|
||||||
setIsSliding(false);
|
setIsSliding(false);
|
||||||
|
|
||||||
await seek(
|
seek(Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value))));
|
||||||
Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value)))
|
|
||||||
);
|
|
||||||
if (wasPlayingRef.current === true) play();
|
if (wasPlayingRef.current === true) play();
|
||||||
},
|
},
|
||||||
[isVlc]
|
[isVlc]
|
||||||
@@ -339,7 +355,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const newTime = isVlc
|
const newTime = isVlc
|
||||||
? Math.max(0, curr - secondsToMs(settings.rewindSkipTime))
|
? Math.max(0, curr - secondsToMs(settings.rewindSkipTime))
|
||||||
: Math.max(0, ticksToSeconds(curr) - settings.rewindSkipTime);
|
: Math.max(0, ticksToSeconds(curr) - settings.rewindSkipTime);
|
||||||
await seek(newTime);
|
seek(newTime);
|
||||||
if (wasPlayingRef.current === true) play();
|
if (wasPlayingRef.current === true) play();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -357,7 +373,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const newTime = isVlc
|
const newTime = isVlc
|
||||||
? curr + secondsToMs(settings.forwardSkipTime)
|
? curr + secondsToMs(settings.forwardSkipTime)
|
||||||
: ticksToSeconds(curr) + settings.forwardSkipTime;
|
: ticksToSeconds(curr) + settings.forwardSkipTime;
|
||||||
await seek(Math.max(0, newTime));
|
seek(Math.max(0, newTime));
|
||||||
if (wasPlayingRef.current === true) play();
|
if (wasPlayingRef.current === true) play();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -365,11 +381,62 @@ export const Controls: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [settings, isPlaying, isVlc]);
|
}, [settings, isPlaying, isVlc]);
|
||||||
|
|
||||||
|
const goToItem = useCallback(
|
||||||
|
async (itemId: string) => {
|
||||||
|
try {
|
||||||
|
const gotoItem = await getItemById(api, itemId);
|
||||||
|
if (!settings || !gotoItem) return;
|
||||||
|
|
||||||
|
lightHapticFeedback();
|
||||||
|
|
||||||
|
const previousIndexes: previousIndexes = {
|
||||||
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
|
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
mediaSource: newMediaSource,
|
||||||
|
audioIndex: defaultAudioIndex,
|
||||||
|
subtitleIndex: defaultSubtitleIndex,
|
||||||
|
} = getDefaultPlaySettings(
|
||||||
|
gotoItem,
|
||||||
|
settings,
|
||||||
|
previousIndexes,
|
||||||
|
mediaSource ?? undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
itemId: gotoItem.Id ?? "", // Ensure itemId is a string
|
||||||
|
audioIndex: defaultAudioIndex?.toString() ?? "",
|
||||||
|
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
||||||
|
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
||||||
|
bitrateValue: bitrateValue.toString(),
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
if (!bitrateValue) {
|
||||||
|
// @ts-expect-error
|
||||||
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// @ts-expect-error
|
||||||
|
router.replace(`player/transcoding-player?${queryParams}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in gotoEpisode:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[settings, subtitleIndex, audioIndex]
|
||||||
|
);
|
||||||
|
|
||||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||||
setIgnoreSafeAreas((prev) => !prev);
|
setIgnoreSafeAreas((prev) => !prev);
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const switchOnEpisodeMode = useCallback(() => {
|
||||||
|
setEpisodeView(true);
|
||||||
|
if (isPlaying) togglePlay();
|
||||||
|
}, [isPlaying, togglePlay]);
|
||||||
|
|
||||||
const memoizedRenderBubble = useCallback(() => {
|
const memoizedRenderBubble = useCallback(() => {
|
||||||
if (!trickPlayUrl || !trickplayInfo) {
|
if (!trickPlayUrl || !trickplayInfo) {
|
||||||
return null;
|
return null;
|
||||||
@@ -433,69 +500,13 @@ export const Controls: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}, [trickPlayUrl, trickplayInfo, time]);
|
}, [trickPlayUrl, trickplayInfo, time]);
|
||||||
|
|
||||||
const [EpisodeView, setEpisodeView] = useState(false);
|
|
||||||
|
|
||||||
const switchOnEpisodeMode = () => {
|
|
||||||
setEpisodeView(true);
|
|
||||||
if (isPlaying) togglePlay();
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToItem = useCallback(
|
|
||||||
async (itemId: string) => {
|
|
||||||
try {
|
|
||||||
const gotoItem = await getItemById(api, itemId);
|
|
||||||
if (!settings || !gotoItem) return;
|
|
||||||
|
|
||||||
lightHapticFeedback();
|
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
|
||||||
audioIndex: audioIndex ? parseInt(audioIndex) : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
mediaSource: newMediaSource,
|
|
||||||
audioIndex: defaultAudioIndex,
|
|
||||||
subtitleIndex: defaultSubtitleIndex,
|
|
||||||
} = getDefaultPlaySettings(
|
|
||||||
gotoItem,
|
|
||||||
settings,
|
|
||||||
previousIndexes,
|
|
||||||
mediaSource ?? undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
itemId: gotoItem.Id ?? "", // Ensure itemId is a string
|
|
||||||
audioIndex: defaultAudioIndex?.toString() ?? "",
|
|
||||||
subtitleIndex: defaultSubtitleIndex?.toString() ?? "",
|
|
||||||
mediaSourceId: newMediaSource?.Id ?? "", // Ensure mediaSourceId is a string
|
|
||||||
bitrateValue: bitrateValue.toString(),
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
if (!bitrateValue) {
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @ts-expect-error
|
|
||||||
router.replace(`player/transcoding-player?${queryParams}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in gotoEpisode:", error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[settings, subtitleIndex, audioIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Used when user changes audio through audio button on device.
|
|
||||||
const [showAudioSlider, setShowAudioSlider] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlProvider
|
<ControlProvider
|
||||||
item={item}
|
item={item}
|
||||||
mediaSource={mediaSource}
|
mediaSource={mediaSource}
|
||||||
isVideoLoaded={isVideoLoaded}
|
isVideoLoaded={isVideoLoaded}
|
||||||
>
|
>
|
||||||
{EpisodeView ? (
|
{episodeView ? (
|
||||||
<EpisodeList
|
<EpisodeList
|
||||||
item={item}
|
item={item}
|
||||||
close={() => setEpisodeView(false)}
|
close={() => setEpisodeView(false)}
|
||||||
@@ -503,23 +514,12 @@ export const Controls: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Pressable
|
<VideoTouchOverlay
|
||||||
onPressIn={() => {
|
screenWidth={screenWidth}
|
||||||
toggleControls();
|
screenHeight={screenHeight}
|
||||||
}}
|
showControls={showControls}
|
||||||
style={{
|
onToggleControls={toggleControls}
|
||||||
position: "absolute",
|
/>
|
||||||
width: screenWidth,
|
|
||||||
height: screenHeight,
|
|
||||||
backgroundColor: "black",
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
opacity: showControls ? 0.5 : 0,
|
|
||||||
}}
|
|
||||||
></Pressable>
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
@@ -731,6 +731,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
className={`flex flex-col p-4`}
|
className={`flex flex-col p-4`}
|
||||||
|
onTouchStart={handleControlsInteraction}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className="shrink flex flex-col justify-center h-full mb-2"
|
className="shrink flex flex-col justify-center h-full mb-2"
|
||||||
|
|||||||
38
components/video-player/controls/VideoTouchOverlay.tsx
Normal file
38
components/video-player/controls/VideoTouchOverlay.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Pressable } from "react-native";
|
||||||
|
import { useTapDetection } from "./useTapDetection";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
screenWidth: number;
|
||||||
|
screenHeight: number;
|
||||||
|
showControls: boolean;
|
||||||
|
onToggleControls: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoTouchOverlay = ({
|
||||||
|
screenWidth,
|
||||||
|
screenHeight,
|
||||||
|
showControls,
|
||||||
|
onToggleControls,
|
||||||
|
}: Props) => {
|
||||||
|
const { handleTouchStart, handleTouchEnd } = useTapDetection({
|
||||||
|
onValidTap: onToggleControls,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: screenWidth,
|
||||||
|
height: screenHeight,
|
||||||
|
backgroundColor: "black",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
opacity: showControls ? 0.5 : 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
56
components/video-player/controls/useControlsTimeout.ts
Normal file
56
components/video-player/controls/useControlsTimeout.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface UseControlsTimeoutProps {
|
||||||
|
showControls: boolean;
|
||||||
|
isSliding: boolean;
|
||||||
|
episodeView: boolean;
|
||||||
|
onHideControls: () => void;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useControlsTimeout = ({
|
||||||
|
showControls,
|
||||||
|
isSliding,
|
||||||
|
episodeView,
|
||||||
|
onHideControls,
|
||||||
|
timeout = 4000,
|
||||||
|
}: UseControlsTimeoutProps) => {
|
||||||
|
const controlsTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resetControlsTimeout = () => {
|
||||||
|
if (controlsTimeoutRef.current) {
|
||||||
|
clearTimeout(controlsTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showControls && !isSliding && !episodeView) {
|
||||||
|
controlsTimeoutRef.current = setTimeout(() => {
|
||||||
|
onHideControls();
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resetControlsTimeout();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (controlsTimeoutRef.current) {
|
||||||
|
clearTimeout(controlsTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [showControls, isSliding, episodeView, timeout, onHideControls]);
|
||||||
|
|
||||||
|
const handleControlsInteraction = () => {
|
||||||
|
if (showControls) {
|
||||||
|
if (controlsTimeoutRef.current) {
|
||||||
|
clearTimeout(controlsTimeoutRef.current);
|
||||||
|
}
|
||||||
|
controlsTimeoutRef.current = setTimeout(() => {
|
||||||
|
onHideControls();
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleControlsInteraction,
|
||||||
|
};
|
||||||
|
};
|
||||||
48
components/video-player/controls/useTapDetection.tsx
Normal file
48
components/video-player/controls/useTapDetection.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { GestureResponderEvent } from "react-native";
|
||||||
|
|
||||||
|
interface TapDetectionOptions {
|
||||||
|
maxDuration?: number;
|
||||||
|
maxDistance?: number;
|
||||||
|
onValidTap?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTapDetection = ({
|
||||||
|
maxDuration = 200,
|
||||||
|
maxDistance = 10,
|
||||||
|
onValidTap,
|
||||||
|
}: TapDetectionOptions = {}) => {
|
||||||
|
const touchStartTime = useRef(0);
|
||||||
|
const touchStartPosition = useRef({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const handleTouchStart = (event: GestureResponderEvent) => {
|
||||||
|
touchStartTime.current = Date.now();
|
||||||
|
touchStartPosition.current = {
|
||||||
|
x: event.nativeEvent.pageX,
|
||||||
|
y: event.nativeEvent.pageY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (event: GestureResponderEvent) => {
|
||||||
|
const touchEndTime = Date.now();
|
||||||
|
const touchEndPosition = {
|
||||||
|
x: event.nativeEvent.pageX,
|
||||||
|
y: event.nativeEvent.pageY,
|
||||||
|
};
|
||||||
|
|
||||||
|
const touchDuration = touchEndTime - touchStartTime.current;
|
||||||
|
const touchDistance = Math.sqrt(
|
||||||
|
Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) +
|
||||||
|
Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (touchDuration < maxDuration && touchDistance < maxDistance) {
|
||||||
|
onValidTap?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleTouchStart,
|
||||||
|
handleTouchEnd,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user