mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-17 09:53:02 +01:00
feat: fade in the controls (instead of on/off toggle)
This commit is contained in:
@@ -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 && (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user