mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-02 12:08:37 +01:00
refactor(casting): extract useCastDismissGesture hook
This commit is contained in:
@@ -8,7 +8,7 @@ import { useAtomValue } from "jotai";
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { GestureDetector } from "react-native-gesture-handler";
|
||||||
import GoogleCast, {
|
import GoogleCast, {
|
||||||
CastState,
|
CastState,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
@@ -17,12 +17,7 @@ import GoogleCast, {
|
|||||||
useMediaStatus,
|
useMediaStatus,
|
||||||
useRemoteMediaClient,
|
useRemoteMediaClient,
|
||||||
} from "react-native-google-cast";
|
} from "react-native-google-cast";
|
||||||
import Animated, {
|
import Animated, { useSharedValue } from "react-native-reanimated";
|
||||||
runOnJS,
|
|
||||||
useAnimatedStyle,
|
|
||||||
useSharedValue,
|
|
||||||
withSpring,
|
|
||||||
} from "react-native-reanimated";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
import { CastPlayerEpisodeControls } from "@/components/casting/player/CastPlayerEpisodeControls";
|
import { CastPlayerEpisodeControls } from "@/components/casting/player/CastPlayerEpisodeControls";
|
||||||
@@ -36,6 +31,7 @@ import { ChromecastEpisodeList } from "@/components/chromecast/ChromecastEpisode
|
|||||||
import { ChromecastSettingsMenu } from "@/components/chromecast/ChromecastSettingsMenu";
|
import { ChromecastSettingsMenu } from "@/components/chromecast/ChromecastSettingsMenu";
|
||||||
import { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments";
|
import { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { useCastDismissGesture } from "@/hooks/useCastDismissGesture";
|
||||||
import { useCastEpisodes } from "@/hooks/useCastEpisodes";
|
import { useCastEpisodes } from "@/hooks/useCastEpisodes";
|
||||||
import { useCasting } from "@/hooks/useCasting";
|
import { useCasting } from "@/hooks/useCasting";
|
||||||
import { useCastPlayerItem } from "@/hooks/useCastPlayerItem";
|
import { useCastPlayerItem } from "@/hooks/useCastPlayerItem";
|
||||||
@@ -333,51 +329,9 @@ export default function CastingPlayerScreen() {
|
|||||||
useChromecastSegments(currentItem, progress * 1000, false);
|
useChromecastSegments(currentItem, progress * 1000, false);
|
||||||
|
|
||||||
// Swipe down to dismiss gesture
|
// Swipe down to dismiss gesture
|
||||||
const translateY = useSharedValue(0);
|
const { panGesture, animatedStyle, dismissModal } = useCastDismissGesture({
|
||||||
const context = useSharedValue({ y: 0 });
|
router,
|
||||||
|
});
|
||||||
const dismissModal = useCallback(() => {
|
|
||||||
// Navigate immediately without animation to prevent crashes
|
|
||||||
if (router.canGoBack()) {
|
|
||||||
router.back();
|
|
||||||
} else {
|
|
||||||
router.replace("/(auth)/(tabs)/(home)/");
|
|
||||||
}
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const panGesture = Gesture.Pan()
|
|
||||||
.onStart(() => {
|
|
||||||
context.value = { y: translateY.value };
|
|
||||||
})
|
|
||||||
.onUpdate((event) => {
|
|
||||||
// Only allow downward swipes from top of screen
|
|
||||||
if (event.translationY > 0) {
|
|
||||||
translateY.value = context.value.y + event.translationY;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onEnd((event) => {
|
|
||||||
// Dismiss if swiped down more than 150px or fast swipe
|
|
||||||
if (event.translationY > 150 || event.velocityY > 600) {
|
|
||||||
// Animate down and dismiss
|
|
||||||
translateY.value = withSpring(
|
|
||||||
1000,
|
|
||||||
{
|
|
||||||
damping: 20,
|
|
||||||
stiffness: 90,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
runOnJS(dismissModal)();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Spring back to position
|
|
||||||
translateY.value = withSpring(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ translateY: translateY.value }],
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Memoize expensive calculations (before early return)
|
// Memoize expensive calculations (before early return)
|
||||||
const posterUrl = useMemo(() => {
|
const posterUrl = useMemo(() => {
|
||||||
|
|||||||
69
hooks/useCastDismissGesture.ts
Normal file
69
hooks/useCastDismissGesture.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { Router } from "expo-router";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { Gesture } from "react-native-gesture-handler";
|
||||||
|
import {
|
||||||
|
runOnJS,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withSpring,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
|
||||||
|
interface UseCastDismissGestureParams {
|
||||||
|
router: Router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swipe-down-to-dismiss gesture cluster for the casting player modal.
|
||||||
|
* Owns the `translateY`/`context` shared values, the pan gesture, the animated
|
||||||
|
* style, and the `dismissModal` callback (also invoked by the header button).
|
||||||
|
*/
|
||||||
|
export function useCastDismissGesture({ router }: UseCastDismissGestureParams) {
|
||||||
|
// Swipe down to dismiss gesture
|
||||||
|
const translateY = useSharedValue(0);
|
||||||
|
const context = useSharedValue({ y: 0 });
|
||||||
|
|
||||||
|
const dismissModal = useCallback(() => {
|
||||||
|
// Navigate immediately without animation to prevent crashes
|
||||||
|
if (router.canGoBack()) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.replace("/(auth)/(tabs)/(home)/");
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const panGesture = Gesture.Pan()
|
||||||
|
.onStart(() => {
|
||||||
|
context.value = { y: translateY.value };
|
||||||
|
})
|
||||||
|
.onUpdate((event) => {
|
||||||
|
// Only allow downward swipes from top of screen
|
||||||
|
if (event.translationY > 0) {
|
||||||
|
translateY.value = context.value.y + event.translationY;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onEnd((event) => {
|
||||||
|
// Dismiss if swiped down more than 150px or fast swipe
|
||||||
|
if (event.translationY > 150 || event.velocityY > 600) {
|
||||||
|
// Animate down and dismiss
|
||||||
|
translateY.value = withSpring(
|
||||||
|
1000,
|
||||||
|
{
|
||||||
|
damping: 20,
|
||||||
|
stiffness: 90,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
runOnJS(dismissModal)();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Spring back to position
|
||||||
|
translateY.value = withSpring(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ translateY: translateY.value }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { panGesture, animatedStyle, dismissModal };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user