mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:33:26 +00:00
improve volume and brightness gestures
This commit is contained in:
@@ -38,14 +38,14 @@ export const Header = () => {
|
||||
: ""}
|
||||
</Text>
|
||||
<View
|
||||
height={48}
|
||||
width={144}
|
||||
height="$3.5"
|
||||
width="$11"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={2}
|
||||
paddingHorizontal={16}
|
||||
paddingVertical={8}
|
||||
paddingHorizontal="$4"
|
||||
paddingVertical="$1"
|
||||
opacity={0.8}
|
||||
backgroundColor="$pillBackground"
|
||||
borderRadius={24}
|
||||
|
@@ -1,14 +1,23 @@
|
||||
import type { AVPlaybackSource } from "expo-av";
|
||||
import type { SharedValue } from "react-native-reanimated";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dimensions, Platform } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { runOnJS, useSharedValue } from "react-native-reanimated";
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedProps,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { ResizeMode, Video } from "expo-av";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { useRouter } from "expo-router";
|
||||
import * as StatusBar from "expo-status-bar";
|
||||
import { Spinner, Text, View } from "tamagui";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Progress, Spinner, Text, useTheme, View } from "tamagui";
|
||||
|
||||
import { findHighestQuality } from "@movie-web/provider-utils";
|
||||
|
||||
@@ -22,18 +31,16 @@ import { useAudioTrackStore } from "~/stores/audio";
|
||||
import { usePlayerStore } from "~/stores/player/store";
|
||||
import { CaptionRenderer } from "./CaptionRenderer";
|
||||
import { ControlsOverlay } from "./ControlsOverlay";
|
||||
import { isPointInSliderVicinity } from "./VideoSlider";
|
||||
|
||||
export const VideoPlayer = () => {
|
||||
const {
|
||||
brightness,
|
||||
showBrightnessOverlay,
|
||||
currentBrightness,
|
||||
setShowBrightnessOverlay,
|
||||
handleBrightnessChange,
|
||||
} = useBrightness();
|
||||
const { showVolumeOverlay, setShowVolumeOverlay, volume, currentVolume } =
|
||||
useVolume();
|
||||
const { volume, showVolumeOverlay, setShowVolumeOverlay } = useVolume();
|
||||
|
||||
const { currentSpeed } = usePlaybackSpeed();
|
||||
const { synchronizePlayback } = useAudioTrack();
|
||||
const { dismissFullscreenPlayer } = usePlayer();
|
||||
@@ -41,8 +48,8 @@ export const VideoPlayer = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [resizeMode, setResizeMode] = useState(ResizeMode.CONTAIN);
|
||||
const [hasStartedPlaying, setHasStartedPlaying] = useState(false);
|
||||
const isGestureInSliderVicinity = useSharedValue(false);
|
||||
const router = useRouter();
|
||||
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const state = usePlayerStore((state) => state.interface.state);
|
||||
@@ -65,10 +72,6 @@ export const VideoPlayer = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const checkGestureInSliderVicinity = (x: number, y: number) => {
|
||||
isGestureInSliderVicinity.value = isPointInSliderVicinity(x, y);
|
||||
};
|
||||
|
||||
const updateResizeMode = (newMode: ResizeMode) => {
|
||||
setResizeMode(newMode);
|
||||
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
@@ -84,7 +87,7 @@ export const VideoPlayer = () => {
|
||||
});
|
||||
|
||||
const doubleTapGesture = Gesture.Tap()
|
||||
.enabled(gestureControlsEnabled)
|
||||
.enabled(gestureControlsEnabled && isIdle)
|
||||
.numberOfTaps(2)
|
||||
.onEnd(() => {
|
||||
runOnJS(toggleAudio)();
|
||||
@@ -94,12 +97,8 @@ export const VideoPlayer = () => {
|
||||
const screenHalfWidth = Dimensions.get("window").width / 2;
|
||||
|
||||
const panGesture = Gesture.Pan()
|
||||
.enabled(gestureControlsEnabled)
|
||||
.enabled(gestureControlsEnabled && isIdle)
|
||||
.onStart((event) => {
|
||||
runOnJS(checkGestureInSliderVicinity)(event.x, event.y);
|
||||
if (isGestureInSliderVicinity.value) {
|
||||
return;
|
||||
}
|
||||
if (event.x > screenHalfWidth) {
|
||||
runOnJS(setShowVolumeOverlay)(true);
|
||||
} else {
|
||||
@@ -108,9 +107,6 @@ export const VideoPlayer = () => {
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
const divisor = 5000;
|
||||
const panIsInHeaderOrFooter = event.y < 100 || event.y > 400;
|
||||
if (panIsInHeaderOrFooter) return;
|
||||
|
||||
const directionMultiplier = event.velocityY < 0 ? 1 : -1;
|
||||
|
||||
const change = directionMultiplier * Math.abs(event.velocityY / divisor);
|
||||
@@ -271,9 +267,9 @@ export const VideoPlayer = () => {
|
||||
)}
|
||||
<ControlsOverlay isLoading={isLoading} />
|
||||
</View>
|
||||
{showVolumeOverlay && <VolumeOverlay volume={currentVolume} />}
|
||||
{showVolumeOverlay && <GestureOverlay value={volume} type="volume" />}
|
||||
{showBrightnessOverlay && (
|
||||
<BrightnessOverlay brightness={currentBrightness} />
|
||||
<GestureOverlay value={brightness} type="brightness" />
|
||||
)}
|
||||
<CaptionRenderer />
|
||||
</View>
|
||||
@@ -281,36 +277,57 @@ export const VideoPlayer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function BrightnessOverlay(props: { brightness: number }) {
|
||||
return (
|
||||
<View
|
||||
position="absolute"
|
||||
bottom={48}
|
||||
alignSelf="center"
|
||||
borderRadius={999}
|
||||
backgroundColor="black"
|
||||
padding={12}
|
||||
opacity={0.5}
|
||||
>
|
||||
<Text fontWeight="bold">
|
||||
Brightness: {Math.round(props.brightness * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
function GestureOverlay(props: {
|
||||
value: SharedValue<number>;
|
||||
type: "brightness" | "volume";
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
function VolumeOverlay(props: { volume: number }) {
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
height: `${props.value.value * 100}%`,
|
||||
borderTopLeftRadius: props.value.value >= 0.98 ? 44 : 0,
|
||||
borderTopRightRadius: props.value.value >= 0.98 ? 44 : 0,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<View
|
||||
position="absolute"
|
||||
bottom={48}
|
||||
alignSelf="center"
|
||||
borderRadius={999}
|
||||
backgroundColor="black"
|
||||
padding={12}
|
||||
opacity={0.5}
|
||||
left={props.type === "brightness" ? "$7" : undefined}
|
||||
right={props.type === "volume" ? "$7" : undefined}
|
||||
borderRadius="$4"
|
||||
gap={8}
|
||||
height="50%"
|
||||
>
|
||||
<Text fontWeight="bold">Volume: {Math.round(props.volume * 100)}%</Text>
|
||||
<Feather
|
||||
size={24}
|
||||
color="white"
|
||||
style={{
|
||||
bottom: 20,
|
||||
}}
|
||||
name={props.type === "brightness" ? "sun" : "volume-2"}
|
||||
/>
|
||||
<View
|
||||
width={14}
|
||||
backgroundColor={theme.progressBackground}
|
||||
justifyContent="flex-end"
|
||||
borderRadius="$4"
|
||||
left={4}
|
||||
bottom={20}
|
||||
height="100%"
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
width: "100%",
|
||||
backgroundColor: theme.progressFilled.val,
|
||||
borderBottomRightRadius: 44,
|
||||
borderBottomLeftRadius: 44,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@@ -29,22 +29,6 @@ interface VideoSliderProps {
|
||||
onSlidingComplete?: (value: number) => void;
|
||||
}
|
||||
|
||||
const SLIDER_VICINITY = {
|
||||
x: 20,
|
||||
y: 200,
|
||||
width: Dimensions.get("window").width - 40,
|
||||
height: 20,
|
||||
};
|
||||
|
||||
export const isPointInSliderVicinity = (x: number, y: number) => {
|
||||
return (
|
||||
x >= SLIDER_VICINITY.x &&
|
||||
x <= SLIDER_VICINITY.x + SLIDER_VICINITY.width &&
|
||||
y >= SLIDER_VICINITY.y &&
|
||||
y <= SLIDER_VICINITY.y + SLIDER_VICINITY.height
|
||||
);
|
||||
};
|
||||
|
||||
const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
||||
const theme = useTheme();
|
||||
const tapRef = useRef<TapGestureHandler>(null);
|
||||
|
@@ -1,19 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import * as Brightness from "expo-brightness";
|
||||
import { useDebounceValue } from "tamagui";
|
||||
|
||||
export const useBrightness = () => {
|
||||
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
|
||||
|
||||
const brightness = useSharedValue(0.5);
|
||||
|
||||
const currentBrightness = useDebounceValue(brightness.value, 20);
|
||||
const memoizedBrightness = useMemo(
|
||||
() => currentBrightness,
|
||||
[currentBrightness],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
try {
|
||||
@@ -42,7 +35,6 @@ export const useBrightness = () => {
|
||||
showBrightnessOverlay,
|
||||
setShowBrightnessOverlay,
|
||||
brightness,
|
||||
currentBrightness: memoizedBrightness,
|
||||
handleBrightnessChange,
|
||||
} as const;
|
||||
};
|
||||
|
@@ -1,19 +1,14 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import { useDebounceValue } from "tamagui";
|
||||
|
||||
export const useVolume = () => {
|
||||
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
|
||||
|
||||
const volume = useSharedValue(1);
|
||||
|
||||
const currentVolume = useDebounceValue(volume.value, 20);
|
||||
const memoizedVolume = useMemo(() => currentVolume, [currentVolume]);
|
||||
|
||||
return {
|
||||
showVolumeOverlay,
|
||||
setShowVolumeOverlay,
|
||||
volume,
|
||||
currentVolume: memoizedVolume,
|
||||
} as const;
|
||||
};
|
||||
|
Reference in New Issue
Block a user