import type { HandlerStateChangeEvent, PanGestureHandlerGestureEvent, TapGestureHandlerEventPayload, } from "react-native-gesture-handler"; import React, { useEffect, useRef } from "react"; import { Dimensions } from "react-native"; import { PanGestureHandler, State, TapGestureHandler, } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, } from "react-native-reanimated"; import { useTheme, View } from "tamagui"; import { usePlayerStore } from "~/stores/player/store"; const clamp = (value: number, lowerBound: number, upperBound: number) => { "worklet"; return Math.min(Math.max(lowerBound, value), upperBound); }; interface VideoSliderProps { onSlidingComplete?: (value: number) => void; } const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => { const theme = useTheme(); const tapRef = useRef(null); const panRef = useRef(null); const status = usePlayerStore((state) => state.status); const setIsIdle = usePlayerStore((state) => state.setIsIdle); const width = Dimensions.get("screen").width - 120; const knobSize_ = 20; const trackSize_ = 8; const minimumValue = 0; const maximumValue = status?.isLoaded ? status.durationMillis! : 0; const value = status?.isLoaded ? status.positionMillis : 0; const valueToX = (v: number) => { if (maximumValue === minimumValue) return 0; return (width * (v - minimumValue)) / (maximumValue - minimumValue); }; const xToValue = (x: number) => { "worklet"; if (maximumValue === minimumValue) return minimumValue; return (x / width) * (maximumValue - minimumValue) + minimumValue; }; const valueX = valueToX(value); const translateX = useSharedValue(valueToX(value)); const isDragging = useSharedValue(false); useEffect(() => { if (!isDragging.value) { translateX.value = clamp(valueX, 0, width - knobSize_); } }, [valueX, isDragging.value, translateX, width]); const _onSlidingComplete = (xValue: number) => { "worklet"; if (onSlidingComplete) runOnJS(onSlidingComplete)(xToValue(xValue)); }; const _onActive = (value: number) => { "worklet"; isDragging.value = true; translateX.value = clamp(value, 0, width - knobSize_); runOnJS(setIsIdle)(false); }; const _onEnd = () => { "worklet"; isDragging.value = false; _onSlidingComplete(translateX.value); }; const onGestureEvent = useAnimatedGestureHandler< PanGestureHandlerGestureEvent, { offsetX: number } >({ onStart: (_, ctx) => (ctx.offsetX = translateX.value), onActive: (event, ctx) => _onActive(event.translationX + ctx.offsetX), onEnd: _onEnd, onCancel: _onEnd, onFinish: _onEnd, }); const onTapEvent = ( event: HandlerStateChangeEvent, ) => { if (event.nativeEvent.state === State.ACTIVE) { _onActive(event.nativeEvent.x); _onSlidingComplete(event.nativeEvent.x); } }; const scrollTranslationStyle = useAnimatedStyle(() => { return { transform: [{ translateX: translateX.value }] }; }); const progressStyle = useAnimatedStyle(() => { return { width: translateX.value + knobSize_, }; }); return ( ); }; export default VideoSlider;