feat: fade in the controls (instead of on/off toggle)

This commit is contained in:
Fredrik Burmester
2025-08-13 15:27:47 +02:00
parent eaf3682384
commit c34c7fbe83
2 changed files with 85 additions and 55 deletions

View File

@@ -25,11 +25,13 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { Slider } from "react-native-awesome-slider"; import { Slider } from "react-native-awesome-slider";
import { import Animated, {
runOnJS, runOnJS,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
@@ -148,11 +150,35 @@ export const Controls: FC<Props> = ({
const min = useSharedValue(0); const min = useSharedValue(0);
const max = useSharedValue(item.RunTimeTicks || 0); const max = useSharedValue(item.RunTimeTicks || 0);
// Animated opacity for smooth transitions
const controlsOpacity = useSharedValue(showControls ? 1 : 0);
const wasPlayingRef = useRef(false); const wasPlayingRef = useRef(false);
const lastProgressRef = useRef<number>(0); const lastProgressRef = useRef<number>(0);
const lightHapticFeedback = useHaptic("light"); const lightHapticFeedback = useHaptic("light");
// Animate controls opacity when showControls changes
useEffect(() => {
controlsOpacity.value = withTiming(showControls ? 1 : 0, {
duration: 300,
});
}, [showControls, controlsOpacity]);
// Animated styles for controls
const animatedControlsStyle = useAnimatedStyle(() => {
return {
opacity: controlsOpacity.value,
};
});
// Animated style for black overlay (75% opacity when visible)
const animatedOverlayStyle = useAnimatedStyle(() => {
return {
opacity: controlsOpacity.value * 0.75,
};
});
useEffect(() => { useEffect(() => {
prefetchAllTrickplayImages(); prefetchAllTrickplayImages();
}, []); }, []);
@@ -708,10 +734,10 @@ export const Controls: FC<Props> = ({
<VideoTouchOverlay <VideoTouchOverlay
screenWidth={screenWidth} screenWidth={screenWidth}
screenHeight={screenHeight} screenHeight={screenHeight}
showControls={showControls}
onToggleControls={toggleControls} onToggleControls={toggleControls}
animatedStyle={animatedOverlayStyle}
/> />
<View <Animated.View
style={[ style={[
{ {
position: "absolute", position: "absolute",
@@ -720,8 +746,8 @@ export const Controls: FC<Props> = ({
width: settings?.safeAreaInControlsEnabled width: settings?.safeAreaInControlsEnabled
? screenWidth - insets.left - insets.right ? screenWidth - insets.left - insets.right
: screenWidth, : screenWidth,
opacity: showControls ? 1 : 0,
}, },
animatedControlsStyle,
]} ]}
pointerEvents={showControls ? "auto" : "none"} pointerEvents={showControls ? "auto" : "none"}
className={"flex flex-row w-full pt-2"} className={"flex flex-row w-full pt-2"}
@@ -803,34 +829,39 @@ export const Controls: FC<Props> = ({
<Ionicons name='close' size={24} color='white' /> <Ionicons name='close' size={24} color='white' />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </Animated.View>
<View <Animated.View
style={{ style={[
position: "absolute", {
top: "50%", // Center vertically position: "absolute",
left: settings?.safeAreaInControlsEnabled ? insets.left : 0, top: "50%", // Center vertically
right: settings?.safeAreaInControlsEnabled ? insets.right : 0, left: settings?.safeAreaInControlsEnabled ? insets.left : 0,
flexDirection: "row", right: settings?.safeAreaInControlsEnabled ? insets.right : 0,
justifyContent: "space-between", flexDirection: "row",
alignItems: "center", justifyContent: "space-between",
transform: [{ translateY: -22.5 }], // Adjust for the button's height (half of 45) alignItems: "center",
paddingHorizontal: "28%", // Add some padding to the left and right transform: [{ translateY: -22.5 }], // Adjust for the button's height (half of 45)
}} paddingHorizontal: 17,
},
animatedControlsStyle,
]}
pointerEvents={showControls ? "box-none" : "none"} pointerEvents={showControls ? "box-none" : "none"}
> >
{/* Brightness Control */}
<View <View
style={{ style={{
position: "absolute", width: 50,
height: 50,
alignItems: "center", alignItems: "center",
transform: [{ rotate: "270deg" }], // Rotate the slider to make it vertical justifyContent: "center",
left: 0, transform: [{ rotate: "270deg" }],
bottom: 30,
opacity: showControls ? 1 : 0,
}} }}
> >
<BrightnessSlider /> <BrightnessSlider />
</View> </View>
{/* Skip Backward */}
{!Platform.isTV && ( {!Platform.isTV && (
<TouchableOpacity onPress={handleSkipBackward}> <TouchableOpacity onPress={handleSkipBackward}>
<View <View
@@ -838,7 +869,6 @@ export const Controls: FC<Props> = ({
position: "relative", position: "relative",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
opacity: showControls ? 1 : 0,
}} }}
> >
<Ionicons <Ionicons
@@ -864,9 +894,8 @@ export const Controls: FC<Props> = ({
</TouchableOpacity> </TouchableOpacity>
)} )}
<View {/* Play/Pause Button */}
style={Platform.isTV ? { flex: 1, alignItems: "center" } : {}} <View style={{ alignItems: "center" }}>
>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
togglePlay(); togglePlay();
@@ -877,9 +906,6 @@ export const Controls: FC<Props> = ({
name={isPlaying ? "pause" : "play"} name={isPlaying ? "pause" : "play"}
size={50} size={50}
color='white' color='white'
style={{
opacity: showControls ? 1 : 0,
}}
/> />
) : ( ) : (
<Loader size={"large"} /> <Loader size={"large"} />
@@ -887,6 +913,7 @@ export const Controls: FC<Props> = ({
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Skip Forward */}
{!Platform.isTV && ( {!Platform.isTV && (
<TouchableOpacity onPress={handleSkipForward}> <TouchableOpacity onPress={handleSkipForward}>
<View <View
@@ -894,7 +921,6 @@ export const Controls: FC<Props> = ({
position: "relative", position: "relative",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
opacity: showControls ? 1 : 0,
}} }}
> >
<Ionicons name='refresh-outline' size={50} color='white' /> <Ionicons name='refresh-outline' size={50} color='white' />
@@ -912,21 +938,23 @@ export const Controls: FC<Props> = ({
</View> </View>
</TouchableOpacity> </TouchableOpacity>
)} )}
{/* Volume/Audio Control */}
<View <View
style={{ style={{
position: "absolute", width: 50,
height: 50,
alignItems: "center", alignItems: "center",
transform: [{ rotate: "270deg" }], // Rotate the slider to make it vertical justifyContent: "center",
bottom: 30, transform: [{ rotate: "270deg" }],
right: 0,
opacity: showAudioSlider || showControls ? 1 : 0, opacity: showAudioSlider || showControls ? 1 : 0,
}} }}
> >
<AudioSlider setVisibility={setShowAudioSlider} /> <AudioSlider setVisibility={setShowAudioSlider} />
</View> </View>
</View> </Animated.View>
<View <Animated.View
style={[ style={[
{ {
position: "absolute", position: "absolute",
@@ -934,6 +962,7 @@ export const Controls: FC<Props> = ({
left: settings?.safeAreaInControlsEnabled ? insets.left : 0, left: settings?.safeAreaInControlsEnabled ? insets.left : 0,
bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0, bottom: settings?.safeAreaInControlsEnabled ? insets.bottom : 0,
}, },
animatedControlsStyle,
]} ]}
className={"flex flex-col px-2"} className={"flex flex-col px-2"}
onTouchStart={handleControlsInteraction} onTouchStart={handleControlsInteraction}
@@ -949,7 +978,6 @@ export const Controls: FC<Props> = ({
style={{ style={{
flexDirection: "column", flexDirection: "column",
alignSelf: "flex-end", // Shrink height based on content alignSelf: "flex-end", // Shrink height based on content
opacity: showControls ? 1 : 0,
}} }}
pointerEvents={showControls ? "box-none" : "none"} pointerEvents={showControls ? "box-none" : "none"}
> >
@@ -998,9 +1026,6 @@ export const Controls: FC<Props> = ({
</View> </View>
<View <View
className={"flex flex-col-reverse rounded-lg items-center my-2"} className={"flex flex-col-reverse rounded-lg items-center my-2"}
style={{
opacity: showControls ? 1 : 0,
}}
pointerEvents={showControls ? "box-none" : "none"} pointerEvents={showControls ? "box-none" : "none"}
> >
<View className={"flex flex-col w-full shrink"}> <View className={"flex flex-col w-full shrink"}>
@@ -1040,7 +1065,7 @@ export const Controls: FC<Props> = ({
</View> </View>
</View> </View>
</View> </View>
</View> </Animated.View>
</> </>
)} )}
{settings.maxAutoPlayEpisodeCount.value !== -1 && ( {settings.maxAutoPlayEpisodeCount.value !== -1 && (

View File

@@ -1,38 +1,43 @@
import { Pressable } from "react-native"; import { Pressable } from "react-native";
import Animated, { type AnimatedStyle } from "react-native-reanimated";
import { useTapDetection } from "./useTapDetection"; import { useTapDetection } from "./useTapDetection";
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
interface Props { interface Props {
screenWidth: number; screenWidth: number;
screenHeight: number; screenHeight: number;
showControls: boolean;
onToggleControls: () => void; onToggleControls: () => void;
animatedStyle: AnimatedStyle;
} }
export const VideoTouchOverlay = ({ export const VideoTouchOverlay = ({
screenWidth, screenWidth,
screenHeight, screenHeight,
showControls,
onToggleControls, onToggleControls,
animatedStyle,
}: Props) => { }: Props) => {
const { handleTouchStart, handleTouchEnd } = useTapDetection({ const { handleTouchStart, handleTouchEnd } = useTapDetection({
onValidTap: onToggleControls, onValidTap: onToggleControls,
}); });
return ( return (
<Pressable <AnimatedPressable
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
style={{ style={[
position: "absolute", {
width: screenWidth, position: "absolute",
height: screenHeight, width: screenWidth,
backgroundColor: "black", height: screenHeight,
left: 0, backgroundColor: "black",
right: 0, left: 0,
top: 0, right: 0,
bottom: 0, top: 0,
opacity: showControls ? 0.75 : 0, bottom: 0,
}} },
animatedStyle,
]}
/> />
); );
}; };