feat: fade and slide in controls (#964)

This commit is contained in:
Fredrik Burmester
2025-08-20 21:59:09 +02:00
committed by GitHub
parent f2219a1daa
commit 55ce7d8cec
5 changed files with 133 additions and 84 deletions

View File

@@ -27,7 +27,6 @@ import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybac
import { useWebSocket } from "@/hooks/useWebsockets"; import { useWebSocket } from "@/hooks/useWebsockets";
import { VlcPlayerView } from "@/modules"; import { VlcPlayerView } from "@/modules";
import type { import type {
PipStartedPayload,
PlaybackStatePayload, PlaybackStatePayload,
ProgressUpdatePayload, ProgressUpdatePayload,
VlcPlayerViewRef, VlcPlayerViewRef,
@@ -60,7 +59,6 @@ export default function page() {
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
const [isBuffering, setIsBuffering] = useState(true); const [isBuffering, setIsBuffering] = useState(true);
const [isVideoLoaded, setIsVideoLoaded] = useState(false); const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [isPipStarted, setIsPipStarted] = useState(false);
const progress = useSharedValue(0); const progress = useSharedValue(0);
const isSeeking = useSharedValue(false); const isSeeking = useSharedValue(false);
@@ -390,11 +388,6 @@ export default function page() {
], ],
); );
const onPipStarted = useCallback((e: PipStartedPayload) => {
const { pipStarted } = e.nativeEvent;
setIsPipStarted(pipStarted);
}, []);
/** Gets the initial playback position in seconds. */ /** Gets the initial playback position in seconds. */
const startPosition = useMemo(() => { const startPosition = useMemo(() => {
return ticksToSeconds(getInitialPlaybackTicks()); return ticksToSeconds(getInitialPlaybackTicks());
@@ -690,7 +683,6 @@ export default function page() {
onVideoProgress={onProgress} onVideoProgress={onProgress}
progressUpdateInterval={1000} progressUpdateInterval={1000}
onVideoStateChange={onPlaybackStateChanged} onVideoStateChange={onPlaybackStateChanged}
onPipStarted={onPipStarted}
onVideoLoadEnd={() => { onVideoLoadEnd={() => {
setIsVideoLoaded(true); setIsVideoLoaded(true);
}} }}
@@ -704,7 +696,7 @@ export default function page() {
}} }}
/> />
</View> </View>
{!isPipStarted && isMounted === true && item && ( {isMounted === true && item && (
<Controls <Controls
mediaSource={stream?.mediaSource} mediaSource={stream?.mediaSource}
item={item} item={item}

View File

@@ -117,7 +117,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
style={{ style={{
flexDirection: "column", flexDirection: "column",
alignSelf: "flex-end", alignSelf: "flex-end",
opacity: showControls ? 1 : 0,
}} }}
pointerEvents={showControls ? "box-none" : "none"} pointerEvents={showControls ? "box-none" : "none"}
> >
@@ -164,9 +163,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
</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"}>

View File

@@ -55,7 +55,6 @@ export const CenterControls: FC<CenterControlsProps> = ({
transform: [{ rotate: "270deg" }], transform: [{ rotate: "270deg" }],
left: 0, left: 0,
bottom: 30, bottom: 30,
opacity: showControls ? 1 : 0,
}} }}
> >
<BrightnessSlider /> <BrightnessSlider />
@@ -68,7 +67,6 @@ export const CenterControls: FC<CenterControlsProps> = ({
position: "relative", position: "relative",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
opacity: showControls ? 1 : 0,
}} }}
> >
<Ionicons <Ionicons
@@ -101,9 +99,6 @@ export const CenterControls: FC<CenterControlsProps> = ({
name={isPlaying ? "pause" : "play"} name={isPlaying ? "pause" : "play"}
size={ICON_SIZES.CENTER} size={ICON_SIZES.CENTER}
color='white' color='white'
style={{
opacity: showControls ? 1 : 0,
}}
/> />
) : ( ) : (
<Loader size={"large"} /> <Loader size={"large"} />
@@ -118,7 +113,6 @@ export const CenterControls: FC<CenterControlsProps> = ({
position: "relative", position: "relative",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
opacity: showControls ? 1 : 0,
}} }}
> >
<Ionicons <Ionicons

View File

@@ -13,10 +13,13 @@ import {
useState, useState,
} from "react"; } from "react";
import { useWindowDimensions } from "react-native"; import { useWindowDimensions } from "react-native";
import { import Animated, {
Easing,
type SharedValue, type SharedValue,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle,
useSharedValue, useSharedValue,
withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay"; import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
import { useCreditSkipper } from "@/hooks/useCreditSkipper"; import { useCreditSkipper } from "@/hooks/useCreditSkipper";
@@ -130,10 +133,61 @@ 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);
// Animation values for controls
const controlsOpacity = useSharedValue(showControls ? 1 : 0);
const headerTranslateY = useSharedValue(showControls ? 0 : -50);
const bottomTranslateY = useSharedValue(showControls ? 0 : 50);
useEffect(() => { useEffect(() => {
prefetchAllTrickplayImages(); prefetchAllTrickplayImages();
}, [prefetchAllTrickplayImages]); }, [prefetchAllTrickplayImages]);
// Animate controls visibility
useEffect(() => {
const animationConfig = {
duration: 300,
easing: Easing.out(Easing.quad),
};
controlsOpacity.value = withTiming(showControls ? 1 : 0, animationConfig);
headerTranslateY.value = withTiming(
showControls ? 0 : -10,
animationConfig,
);
bottomTranslateY.value = withTiming(showControls ? 0 : 10, animationConfig);
}, [showControls, controlsOpacity, headerTranslateY, bottomTranslateY]);
// Create animated styles
const headerAnimatedStyle = useAnimatedStyle(() => ({
opacity: controlsOpacity.value,
transform: [{ translateY: headerTranslateY.value }],
position: "absolute" as const,
top: 0,
left: 0,
right: 0,
zIndex: 10,
}));
const centerAnimatedStyle = useAnimatedStyle(() => ({
opacity: controlsOpacity.value,
position: "absolute" as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 5,
}));
const bottomAnimatedStyle = useAnimatedStyle(() => ({
opacity: controlsOpacity.value,
transform: [{ translateY: bottomTranslateY.value }],
position: "absolute" as const,
bottom: 0,
left: 0,
right: 0,
zIndex: 10,
}));
// Initialize progress values // Initialize progress values
useEffect(() => { useEffect(() => {
if (item) { if (item) {
@@ -435,68 +489,83 @@ export const Controls: FC<Props> = ({
showControls={showControls} showControls={showControls}
onToggleControls={toggleControls} onToggleControls={toggleControls}
/> />
<HeaderControls <Animated.View
item={item} style={headerAnimatedStyle}
showControls={showControls} pointerEvents={showControls ? "auto" : "none"}
offline={offline} >
mediaSource={mediaSource} <HeaderControls
startPictureInPicture={startPictureInPicture} item={item}
switchOnEpisodeMode={switchOnEpisodeMode} showControls={showControls}
goToPreviousItem={goToPreviousItem} offline={offline}
goToNextItem={goToNextItem} mediaSource={mediaSource}
previousItem={previousItem} startPictureInPicture={startPictureInPicture}
nextItem={nextItem} switchOnEpisodeMode={switchOnEpisodeMode}
getAudioTracks={getAudioTracks} goToPreviousItem={goToPreviousItem}
getSubtitleTracks={getSubtitleTracks} goToNextItem={goToNextItem}
setAudioTrack={setAudioTrack} previousItem={previousItem}
setSubtitleTrack={setSubtitleTrack} nextItem={nextItem}
setSubtitleURL={setSubtitleURL} getAudioTracks={getAudioTracks}
aspectRatio={aspectRatio} getSubtitleTracks={getSubtitleTracks}
scaleFactor={scaleFactor} setAudioTrack={setAudioTrack}
setAspectRatio={setAspectRatio} setSubtitleTrack={setSubtitleTrack}
setScaleFactor={setScaleFactor} setSubtitleURL={setSubtitleURL}
setVideoAspectRatio={setVideoAspectRatio} aspectRatio={aspectRatio}
setVideoScaleFactor={setVideoScaleFactor} scaleFactor={scaleFactor}
/> setAspectRatio={setAspectRatio}
<CenterControls setScaleFactor={setScaleFactor}
showControls={showControls} setVideoAspectRatio={setVideoAspectRatio}
isPlaying={isPlaying} setVideoScaleFactor={setVideoScaleFactor}
isBuffering={isBuffering} />
showAudioSlider={showAudioSlider} </Animated.View>
setShowAudioSlider={setShowAudioSlider} <Animated.View
togglePlay={togglePlay} style={centerAnimatedStyle}
handleSkipBackward={handleSkipBackward} pointerEvents={showControls ? "box-none" : "none"}
handleSkipForward={handleSkipForward} >
/> <CenterControls
<BottomControls showControls={showControls}
item={item} isPlaying={isPlaying}
showControls={showControls} isBuffering={isBuffering}
isSliding={isSliding} showAudioSlider={showAudioSlider}
showRemoteBubble={showRemoteBubble} setShowAudioSlider={setShowAudioSlider}
currentTime={currentTime} togglePlay={togglePlay}
remainingTime={remainingTime} handleSkipBackward={handleSkipBackward}
isVlc={isVlc} handleSkipForward={handleSkipForward}
showSkipButton={showSkipButton} />
showSkipCreditButton={showSkipCreditButton} </Animated.View>
skipIntro={skipIntro} <Animated.View
skipCredit={skipCredit} style={bottomAnimatedStyle}
nextItem={nextItem} pointerEvents={showControls ? "auto" : "none"}
handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay} >
handleNextEpisodeManual={handleNextEpisodeManual} <BottomControls
handleControlsInteraction={handleControlsInteraction} item={item}
min={min} showControls={showControls}
max={max} isSliding={isSliding}
effectiveProgress={effectiveProgress} showRemoteBubble={showRemoteBubble}
cacheProgress={cacheProgress} currentTime={currentTime}
handleSliderStart={handleSliderStart} remainingTime={remainingTime}
handleSliderComplete={handleSliderComplete} isVlc={isVlc}
handleSliderChange={handleSliderChange} showSkipButton={showSkipButton}
handleTouchStart={handleTouchStart} showSkipCreditButton={showSkipCreditButton}
handleTouchEnd={handleTouchEnd} skipIntro={skipIntro}
trickPlayUrl={trickPlayUrl} skipCredit={skipCredit}
trickplayInfo={trickplayInfo} nextItem={nextItem}
time={isSliding || showRemoteBubble ? time : remoteTime} handleNextEpisodeAutoPlay={handleNextEpisodeAutoPlay}
/> handleNextEpisodeManual={handleNextEpisodeManual}
handleControlsInteraction={handleControlsInteraction}
min={min}
max={max}
effectiveProgress={effectiveProgress}
cacheProgress={cacheProgress}
handleSliderStart={handleSliderStart}
handleSliderComplete={handleSliderComplete}
handleSliderChange={handleSliderChange}
handleTouchStart={handleTouchStart}
handleTouchEnd={handleTouchEnd}
trickPlayUrl={trickPlayUrl}
trickplayInfo={trickplayInfo}
time={isSliding || showRemoteBubble ? time : remoteTime}
/>
</Animated.View>
</> </>
)} )}
{settings.maxAutoPlayEpisodeCount.value !== -1 && ( {settings.maxAutoPlayEpisodeCount.value !== -1 && (

View File

@@ -106,7 +106,6 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
width: settings?.safeAreaInControlsEnabled width: settings?.safeAreaInControlsEnabled
? screenWidth - insets.left - insets.right ? screenWidth - insets.left - insets.right
: screenWidth, : screenWidth,
opacity: showControls ? 1 : 0,
}, },
]} ]}
pointerEvents={showControls ? "auto" : "none"} pointerEvents={showControls ? "auto" : "none"}
@@ -138,7 +137,6 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
name='picture-in-picture' name='picture-in-picture'
size={ICON_SIZES.HEADER} size={ICON_SIZES.HEADER}
color='white' color='white'
style={{ opacity: showControls ? 1 : 0 }}
/> />
</TouchableOpacity> </TouchableOpacity>
)} )}