mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:33:26 +00:00
add buggy videoslider
This commit is contained in:
@@ -8,13 +8,12 @@ import { mapMillisecondsToTime } from "./utils";
|
|||||||
|
|
||||||
export const BottomControls = () => {
|
export const BottomControls = () => {
|
||||||
const status = usePlayerStore((state) => state.status);
|
const status = usePlayerStore((state) => state.status);
|
||||||
status?.isLoaded;
|
|
||||||
|
|
||||||
if (status?.isLoaded) {
|
if (status?.isLoaded) {
|
||||||
return (
|
return (
|
||||||
<Controls>
|
<Controls>
|
||||||
<View className="flex h-16 w-full flex-row items-center justify-center">
|
<View className="flex h-16 w-full flex-row items-center justify-center">
|
||||||
<View className="flex flex-row items-center justify-center gap-5 px-4 py-2">
|
<View className="flex flex-row items-center justify-center px-4 py-2">
|
||||||
<Text className="font-bold">
|
<Text className="font-bold">
|
||||||
{mapMillisecondsToTime(status.positionMillis ?? 0)}
|
{mapMillisecondsToTime(status.positionMillis ?? 0)}
|
||||||
</Text>
|
</Text>
|
||||||
|
@@ -1,79 +1,26 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback } from "react";
|
||||||
import { Dimensions, PanResponder, TouchableOpacity, View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
import VideoSlider from "./VideoSlider";
|
||||||
|
|
||||||
export const ProgressBar = () => {
|
export const ProgressBar = () => {
|
||||||
const status = usePlayerStore((state) => state.status);
|
const status = usePlayerStore((state) => state.status);
|
||||||
const videoRef = usePlayerStore((state) => state.videoRef);
|
const videoRef = usePlayerStore((state) => state.videoRef);
|
||||||
|
|
||||||
const screenWidth = Dimensions.get("window").width;
|
|
||||||
const progressBarWidth = screenWidth - 40; // Adjust the padding as needed
|
|
||||||
|
|
||||||
const updateProgress = useCallback(
|
const updateProgress = useCallback(
|
||||||
(newProgress: number) => {
|
(newProgress: number) => {
|
||||||
videoRef?.setStatusAsync({ positionMillis: newProgress }).catch(() => {
|
videoRef?.setStatusAsync({ positionMillis: newProgress }).catch(() => {
|
||||||
console.log("Error updating progress");
|
console.error("Error updating progress");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[videoRef],
|
[videoRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
const panResponder = useRef(
|
|
||||||
PanResponder.create({
|
|
||||||
onStartShouldSetPanResponder: () => true,
|
|
||||||
onMoveShouldSetPanResponder: () => true,
|
|
||||||
onPanResponderMove: (e, gestureState) => {
|
|
||||||
console.log(gestureState.moveX, gestureState.x0, gestureState.dx);
|
|
||||||
},
|
|
||||||
onPanResponderRelease: (e, gestureState) => {
|
|
||||||
console.log("onPanResponderRelease");
|
|
||||||
const { moveX, x0 } = gestureState;
|
|
||||||
const newProgress = (moveX - x0) / progressBarWidth;
|
|
||||||
updateProgress(newProgress);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).current;
|
|
||||||
|
|
||||||
if (status?.isLoaded) {
|
if (status?.isLoaded) {
|
||||||
const progressRatio =
|
|
||||||
status.durationMillis && status.durationMillis !== 0
|
|
||||||
? status.positionMillis / status.durationMillis
|
|
||||||
: 0;
|
|
||||||
return (
|
return (
|
||||||
<View className="flex h-8 flex-1 items-center justify-center">
|
<View className="flex h-10 flex-1 items-center justify-center p-8">
|
||||||
{/* Progress Dot */}
|
<VideoSlider onSlidingComplete={updateProgress} />
|
||||||
<View className="absolute inset-x-0 top-0">
|
|
||||||
<View
|
|
||||||
className="z-10 h-4 w-4 rounded-full bg-primary-100"
|
|
||||||
style={{
|
|
||||||
left: `${progressRatio * 100}%`,
|
|
||||||
transform: [
|
|
||||||
{ translateY: 7 },
|
|
||||||
{
|
|
||||||
translateX: -4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Full bar */}
|
|
||||||
<TouchableOpacity
|
|
||||||
className="relative h-1 w-full rounded-full bg-secondary-300 bg-opacity-25 transition-[height] duration-100"
|
|
||||||
{...panResponder.panHandlers}
|
|
||||||
>
|
|
||||||
{/* TODO: Preloaded */}
|
|
||||||
<View className="absolute left-0 top-0 h-full rounded-full bg-secondary-300" />
|
|
||||||
|
|
||||||
{/* Progress */}
|
|
||||||
<View
|
|
||||||
className="dir-neutral:left-0 absolute top-0 flex h-full items-center justify-end rounded-full bg-primary-100"
|
|
||||||
style={{
|
|
||||||
width: `${progressRatio * 100}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
167
apps/expo/src/components/player/VideoSlider.tsx
Normal file
167
apps/expo/src/components/player/VideoSlider.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import type {
|
||||||
|
HandlerStateChangeEvent,
|
||||||
|
PanGestureHandlerGestureEvent,
|
||||||
|
TapGestureHandlerEventPayload,
|
||||||
|
} from "react-native-gesture-handler";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { Dimensions, StyleSheet, View } from "react-native";
|
||||||
|
import {
|
||||||
|
PanGestureHandler,
|
||||||
|
State,
|
||||||
|
TapGestureHandler,
|
||||||
|
} from "react-native-gesture-handler";
|
||||||
|
import Animated, {
|
||||||
|
runOnJS,
|
||||||
|
useAnimatedGestureHandler,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
|
||||||
|
import colors from "@movie-web/tailwind-config/colors";
|
||||||
|
|
||||||
|
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 status = usePlayerStore((state) => state.status);
|
||||||
|
|
||||||
|
const width = Dimensions.get("screen").width - 140;
|
||||||
|
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 tapRef = useRef<TapGestureHandler>(null);
|
||||||
|
const panRef = useRef<PanGestureHandler>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
translateX.value = clamp(valueX, 0, width - knobSize_);
|
||||||
|
}, [valueX]);
|
||||||
|
|
||||||
|
const _onSlidingComplete = (xValue: number) => {
|
||||||
|
"worklet";
|
||||||
|
if (onSlidingComplete) runOnJS(onSlidingComplete)(xToValue(xValue));
|
||||||
|
};
|
||||||
|
|
||||||
|
const _onActive = (value: number) => {
|
||||||
|
"worklet";
|
||||||
|
translateX.value = clamp(value, 0, width - knobSize_);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGestureEvent = useAnimatedGestureHandler<
|
||||||
|
PanGestureHandlerGestureEvent,
|
||||||
|
{ offsetX: number }
|
||||||
|
>({
|
||||||
|
onStart: (_, ctx) => (ctx.offsetX = translateX.value),
|
||||||
|
onActive: (event, ctx) => _onActive(event.translationX + ctx.offsetX),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onTapEvent = (
|
||||||
|
event: HandlerStateChangeEvent<TapGestureHandlerEventPayload>,
|
||||||
|
) => {
|
||||||
|
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 (
|
||||||
|
<TapGestureHandler
|
||||||
|
ref={tapRef}
|
||||||
|
onHandlerStateChange={onTapEvent}
|
||||||
|
simultaneousHandlers={panRef}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: knobSize_,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className="justify-center"
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
height: trackSize_,
|
||||||
|
borderRadius: trackSize_,
|
||||||
|
backgroundColor: colors.secondary[700],
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
className="absolute bottom-0 left-0 right-0 top-0"
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
backgroundColor: colors.primary[300],
|
||||||
|
borderRadius: trackSize_ / 2,
|
||||||
|
},
|
||||||
|
progressStyle,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<PanGestureHandler
|
||||||
|
ref={panRef}
|
||||||
|
onGestureEvent={onGestureEvent}
|
||||||
|
simultaneousHandlers={tapRef}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.knob,
|
||||||
|
{
|
||||||
|
height: knobSize_,
|
||||||
|
width: knobSize_,
|
||||||
|
borderRadius: knobSize_ / 2,
|
||||||
|
backgroundColor: colors.primary[300],
|
||||||
|
},
|
||||||
|
scrollTranslationStyle,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</PanGestureHandler>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TapGestureHandler>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
knob: {
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VideoSlider;
|
Reference in New Issue
Block a user