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>
|
</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}
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user