improve volume and brightness gestures

This commit is contained in:
Jorrin
2024-03-22 19:55:36 +01:00
parent 945a9bf21d
commit f2fe68c31a
5 changed files with 70 additions and 82 deletions

View File

@@ -38,14 +38,14 @@ export const Header = () => {
: ""} : ""}
</Text> </Text>
<View <View
height={48} height="$3.5"
width={144} width="$11"
flexDirection="row" flexDirection="row"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
gap={2} gap={2}
paddingHorizontal={16} paddingHorizontal="$4"
paddingVertical={8} paddingVertical="$1"
opacity={0.8} opacity={0.8}
backgroundColor="$pillBackground" backgroundColor="$pillBackground"
borderRadius={24} borderRadius={24}

View File

@@ -1,14 +1,23 @@
import type { AVPlaybackSource } from "expo-av"; import type { AVPlaybackSource } from "expo-av";
import type { SharedValue } from "react-native-reanimated";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Dimensions, Platform } from "react-native"; import { Dimensions, Platform } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler"; 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 { ResizeMode, Video } from "expo-av";
import * as Haptics from "expo-haptics"; import * as Haptics from "expo-haptics";
import * as NavigationBar from "expo-navigation-bar"; import * as NavigationBar from "expo-navigation-bar";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import * as StatusBar from "expo-status-bar"; 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"; import { findHighestQuality } from "@movie-web/provider-utils";
@@ -22,18 +31,16 @@ import { useAudioTrackStore } from "~/stores/audio";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
import { CaptionRenderer } from "./CaptionRenderer"; import { CaptionRenderer } from "./CaptionRenderer";
import { ControlsOverlay } from "./ControlsOverlay"; import { ControlsOverlay } from "./ControlsOverlay";
import { isPointInSliderVicinity } from "./VideoSlider";
export const VideoPlayer = () => { export const VideoPlayer = () => {
const { const {
brightness, brightness,
showBrightnessOverlay, showBrightnessOverlay,
currentBrightness,
setShowBrightnessOverlay, setShowBrightnessOverlay,
handleBrightnessChange, handleBrightnessChange,
} = useBrightness(); } = useBrightness();
const { showVolumeOverlay, setShowVolumeOverlay, volume, currentVolume } = const { volume, showVolumeOverlay, setShowVolumeOverlay } = useVolume();
useVolume();
const { currentSpeed } = usePlaybackSpeed(); const { currentSpeed } = usePlaybackSpeed();
const { synchronizePlayback } = useAudioTrack(); const { synchronizePlayback } = useAudioTrack();
const { dismissFullscreenPlayer } = usePlayer(); const { dismissFullscreenPlayer } = usePlayer();
@@ -41,8 +48,8 @@ export const VideoPlayer = () => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [resizeMode, setResizeMode] = useState(ResizeMode.CONTAIN); const [resizeMode, setResizeMode] = useState(ResizeMode.CONTAIN);
const [hasStartedPlaying, setHasStartedPlaying] = useState(false); const [hasStartedPlaying, setHasStartedPlaying] = useState(false);
const isGestureInSliderVicinity = useSharedValue(false);
const router = useRouter(); const router = useRouter();
const scale = useSharedValue(1); const scale = useSharedValue(1);
const state = usePlayerStore((state) => state.interface.state); 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) => { const updateResizeMode = (newMode: ResizeMode) => {
setResizeMode(newMode); setResizeMode(newMode);
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
@@ -84,7 +87,7 @@ export const VideoPlayer = () => {
}); });
const doubleTapGesture = Gesture.Tap() const doubleTapGesture = Gesture.Tap()
.enabled(gestureControlsEnabled) .enabled(gestureControlsEnabled && isIdle)
.numberOfTaps(2) .numberOfTaps(2)
.onEnd(() => { .onEnd(() => {
runOnJS(toggleAudio)(); runOnJS(toggleAudio)();
@@ -94,12 +97,8 @@ export const VideoPlayer = () => {
const screenHalfWidth = Dimensions.get("window").width / 2; const screenHalfWidth = Dimensions.get("window").width / 2;
const panGesture = Gesture.Pan() const panGesture = Gesture.Pan()
.enabled(gestureControlsEnabled) .enabled(gestureControlsEnabled && isIdle)
.onStart((event) => { .onStart((event) => {
runOnJS(checkGestureInSliderVicinity)(event.x, event.y);
if (isGestureInSliderVicinity.value) {
return;
}
if (event.x > screenHalfWidth) { if (event.x > screenHalfWidth) {
runOnJS(setShowVolumeOverlay)(true); runOnJS(setShowVolumeOverlay)(true);
} else { } else {
@@ -108,9 +107,6 @@ export const VideoPlayer = () => {
}) })
.onUpdate((event) => { .onUpdate((event) => {
const divisor = 5000; const divisor = 5000;
const panIsInHeaderOrFooter = event.y < 100 || event.y > 400;
if (panIsInHeaderOrFooter) return;
const directionMultiplier = event.velocityY < 0 ? 1 : -1; const directionMultiplier = event.velocityY < 0 ? 1 : -1;
const change = directionMultiplier * Math.abs(event.velocityY / divisor); const change = directionMultiplier * Math.abs(event.velocityY / divisor);
@@ -271,9 +267,9 @@ export const VideoPlayer = () => {
)} )}
<ControlsOverlay isLoading={isLoading} /> <ControlsOverlay isLoading={isLoading} />
</View> </View>
{showVolumeOverlay && <VolumeOverlay volume={currentVolume} />} {showVolumeOverlay && <GestureOverlay value={volume} type="volume" />}
{showBrightnessOverlay && ( {showBrightnessOverlay && (
<BrightnessOverlay brightness={currentBrightness} /> <GestureOverlay value={brightness} type="brightness" />
)} )}
<CaptionRenderer /> <CaptionRenderer />
</View> </View>
@@ -281,36 +277,57 @@ export const VideoPlayer = () => {
); );
}; };
function BrightnessOverlay(props: { brightness: number }) { function GestureOverlay(props: {
return ( value: SharedValue<number>;
<View type: "brightness" | "volume";
position="absolute" }) {
bottom={48} const theme = useTheme();
alignSelf="center"
borderRadius={999}
backgroundColor="black"
padding={12}
opacity={0.5}
>
<Text fontWeight="bold">
Brightness: {Math.round(props.brightness * 100)}%
</Text>
</View>
);
}
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 ( return (
<View <View
position="absolute" position="absolute"
bottom={48} left={props.type === "brightness" ? "$7" : undefined}
alignSelf="center" right={props.type === "volume" ? "$7" : undefined}
borderRadius={999} borderRadius="$4"
backgroundColor="black" gap={8}
padding={12} height="50%"
opacity={0.5}
> >
<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> </View>
); );
} }

View File

@@ -29,22 +29,6 @@ interface VideoSliderProps {
onSlidingComplete?: (value: number) => void; 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 VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
const theme = useTheme(); const theme = useTheme();
const tapRef = useRef<TapGestureHandler>(null); const tapRef = useRef<TapGestureHandler>(null);

View File

@@ -1,19 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useSharedValue } from "react-native-reanimated"; import { useSharedValue } from "react-native-reanimated";
import * as Brightness from "expo-brightness"; import * as Brightness from "expo-brightness";
import { useDebounceValue } from "tamagui";
export const useBrightness = () => { export const useBrightness = () => {
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false); const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
const brightness = useSharedValue(0.5); const brightness = useSharedValue(0.5);
const currentBrightness = useDebounceValue(brightness.value, 20);
const memoizedBrightness = useMemo(
() => currentBrightness,
[currentBrightness],
);
useEffect(() => { useEffect(() => {
async function init() { async function init() {
try { try {
@@ -42,7 +35,6 @@ export const useBrightness = () => {
showBrightnessOverlay, showBrightnessOverlay,
setShowBrightnessOverlay, setShowBrightnessOverlay,
brightness, brightness,
currentBrightness: memoizedBrightness,
handleBrightnessChange, handleBrightnessChange,
} as const; } as const;
}; };

View File

@@ -1,19 +1,14 @@
import { useMemo, useState } from "react"; import { useState } from "react";
import { useSharedValue } from "react-native-reanimated"; import { useSharedValue } from "react-native-reanimated";
import { useDebounceValue } from "tamagui";
export const useVolume = () => { export const useVolume = () => {
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false); const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
const volume = useSharedValue(1); const volume = useSharedValue(1);
const currentVolume = useDebounceValue(volume.value, 20);
const memoizedVolume = useMemo(() => currentVolume, [currentVolume]);
return { return {
showVolumeOverlay, showVolumeOverlay,
setShowVolumeOverlay, setShowVolumeOverlay,
volume, volume,
currentVolume: memoizedVolume,
} as const; } as const;
}; };