mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-19 02:34:17 +01:00
feat: fade and slide in controls (#964)
This commit is contained in:
committed by
GitHub
parent
f2219a1daa
commit
55ce7d8cec
@@ -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}
|
||||||
|
|||||||
@@ -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"}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user